This commit is contained in:
ch197511161
2025-12-11 01:29:41 +08:00
parent 908b4361ed
commit ddce8fce18
473 changed files with 267270 additions and 0 deletions

235
app/.vscode/css_custom_data.json vendored Normal file
View File

@@ -0,0 +1,235 @@
{
"version": 1.1,
"properties": [
{
"name": "nine-slice",
"description": "九宫格边框类 - 支持基础到高级功能",
"references": [
{
"name": "MDN Reference",
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-image"
}
]
},
{
"name": "nine-slice-no-fill",
"description": "九宫格边框类 - 不填充中心区域"
},
{
"name": "nine-slice-animated",
"description": "九宫格边框类 - 支持动画过渡"
},
{
"name": "dashboard-container",
"description": "大屏布局容器 - 全屏深色背景"
},
{
"name": "dashboard-grid",
"description": "大屏网格布局"
},
{
"name": "dashboard-grid-2x2",
"description": "2x2 大屏网格布局"
},
{
"name": "dashboard-grid-3x3",
"description": "3x3 大屏网格布局"
},
{
"name": "dashboard-grid-4x4",
"description": "4x4 大屏网格布局"
},
{
"name": "dashboard-card",
"description": "大屏卡片容器 - 半透明背景"
},
{
"name": "dashboard-card-highlight",
"description": "高亮大屏卡片 - 蓝色边框"
},
{
"name": "dashboard-card-warning",
"description": "警告大屏卡片 - 橙色边框"
},
{
"name": "dashboard-card-danger",
"description": "危险大屏卡片 - 红色边框"
},
{
"name": "dashboard-title",
"description": "大屏标题样式 - 大号白色文字"
},
{
"name": "dashboard-subtitle",
"description": "大屏副标题样式 - 中号浅色文字"
},
{
"name": "dashboard-metric-title",
"description": "指标标题样式 - 小号灰色文字"
},
{
"name": "dashboard-metric-large",
"description": "大号数值显示 - 6xl 字体"
},
{
"name": "dashboard-metric-medium",
"description": "中号数值显示 - 5xl 字体"
},
{
"name": "dashboard-metric-small",
"description": "小号数值显示 - 3xl 字体"
},
{
"name": "dashboard-metric-trend-up",
"description": "上升趋势指标 - 绿色文字"
},
{
"name": "dashboard-metric-trend-down",
"description": "下降趋势指标 - 红色文字"
},
{
"name": "chart-container",
"description": "图表容器 - 中等尺寸"
},
{
"name": "chart-container-large",
"description": "大尺寸图表容器"
},
{
"name": "chart-container-small",
"description": "小尺寸图表容器"
},
{
"name": "dashboard-progress",
"description": "进度条容器"
},
{
"name": "dashboard-progress-bar",
"description": "进度条 - 蓝色渐变"
},
{
"name": "dashboard-progress-bar-success",
"description": "进度条 - 绿色渐变"
},
{
"name": "dashboard-progress-bar-warning",
"description": "进度条 - 橙色渐变"
},
{
"name": "dashboard-progress-bar-danger",
"description": "进度条 - 红色渐变"
},
{
"name": "status-indicator",
"description": "状态指示器容器"
},
{
"name": "status-online",
"description": "在线状态 - 绿色"
},
{
"name": "status-offline",
"description": "离线状态 - 红色"
},
{
"name": "status-warning",
"description": "警告状态 - 橙色"
},
{
"name": "status-dot",
"description": "状态点 - 基础样式"
},
{
"name": "status-dot-online",
"description": "在线状态点 - 绿色脉冲动画"
},
{
"name": "status-dot-offline",
"description": "离线状态点 - 红色"
},
{
"name": "status-dot-warning",
"description": "警告状态点 - 橙色"
},
{
"name": "dashboard-fade-in",
"description": "淡入动画"
},
{
"name": "dashboard-slide-up",
"description": "从下往上滑入动画"
},
{
"name": "dashboard-scale-in",
"description": "缩放进入动画"
},
{
"name": "animate-fade-in",
"description": "淡入动画 - 自定义关键帧"
},
{
"name": "animate-slide-up",
"description": "滑入动画 - 自定义关键帧"
},
{
"name": "animate-scale-in",
"description": "缩放动画 - 自定义关键帧"
},
{
"name": "dashboard-table",
"description": "大屏数据表格样式"
},
{
"name": "dashboard-scrollbar",
"description": "自定义滚动条样式"
},
{
"name": "dashboard-fullscreen",
"description": "全屏模式样式"
},
{
"name": "dashboard-split-horizontal",
"description": "水平分屏布局"
},
{
"name": "dashboard-split-vertical",
"description": "垂直分屏布局"
},
{
"name": "dashboard-align-top",
"description": "顶部对齐"
},
{
"name": "dashboard-align-bottom",
"description": "底部对齐"
},
{
"name": "dashboard-align-left",
"description": "左侧对齐"
},
{
"name": "dashboard-align-right",
"description": "右侧对齐"
},
{
"name": "bg-flex-center",
"description": "背景图居中覆盖 - 支持自定义padding控制"
},
{
"name": "crud-container",
"description": "CRUD 容器样式"
},
{
"name": "crud-header",
"description": "CRUD 头部样式"
},
{
"name": "crud-table",
"description": "CRUD 表格样式"
},
{
"name": "crud-form",
"description": "CRUD 表单样式"
}
]
}

25
app/app.vue Normal file
View File

@@ -0,0 +1,25 @@
<template>
<div class="h-screen w-screen overflow-hidden bg-black/0">
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<style>
/* 导入 Tailwind CSS */
@import './assets/css/main.css';
html,
body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
overflow: hidden;
font-family:
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑',
Arial, sans-serif;
}
</style>

573
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,573 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 背景图居中 */
/* 背景图居中覆盖 - 支持自定义padding控制 */
.bg-flex-center {
@apply flex items-center justify-center bg-center bg-no-repeat bg-contain;
}
/* 自定义样式 */
.crud-container {
@apply max-w-7xl mx-auto p-6;
}
.crud-header {
@apply mb-6 flex justify-between items-center;
}
.crud-table {
@apply bg-white rounded-lg shadow-sm;
}
.crud-form {
@apply bg-white p-6 rounded-lg shadow-sm;
}
/*
九宫格9-slice图片边框 Utility
目标:
- 传入一张九宫格图片的 URL保持4个角按上下左右的参数不拉伸
- 中间和4个角中间的区域拉伸
- 支持自定义重复模式、填充行为、边框扩展等高级选项
核心参数:
- 图片URL--nine-src
- 切片参数:--nine-top/right/bottom/left基于图片实际像素
- 快速设置:--nine-all一次性设置四个角相等
- 重复模式:--nine-repeatstretch/repeat/round/space
- 填充控制:--nine-fill1 填充/0 不填充)
- 边框扩展:--nine-outset向外扩展距离
- 边框宽度:--nine-width边框图像宽度
使用说明Tailwind 原子类 + CSS 变量):
1) 在元素上添加基础类:`nine-slice`
2) 通过 Tailwind 任意值设置 CSS 变量:
- 设置图片 URL`[--nine-src:url('/public/images/frame-9.png')]`
- 设置切片大小:`[--nine-top:10] [--nine-right:10] [--nine-bottom:10] [--nine-left:10]`
- 快速设置(四个角相等):`[--nine-all:15]`等效于设置四个角都为15
- 可选高级参数:
- 重复模式:`[--nine-repeat:repeat]`(默认 stretch
- 向外扩展:`[--nine-outset:5]`(默认 0
- 边框宽度:`[--nine-width:2]`(默认 1
3) 填充控制:
- 默认:填充中心区域
- 添加 `nine-slice-no-fill` 类:不填充中心,露出背景
示例:
<!-- 基础使用 -->
<div class="nine-slice [--nine-src:url('/public/images/frame-9.png')] [--nine-top:10] [--nine-right:10] [--nine-bottom:10] [--nine-left:10] p-4">
内容区域
</div>
<!-- 快速设置(四个角相等) -->
<div class="nine-slice [--nine-src:url('/public/images/frame-9.png')] [--nine-all:15] p-4">
快速设置 - 四个角都是15px
</div>
<!-- 重复图案 -->
<div class="nine-slice [--nine-src:url('/public/images/pattern.png')] [--nine-top:15] [--nine-right:15] [--nine-bottom:15] [--nine-left:15] [--nine-repeat:round] p-6">
重复图案边框
</div>
<!-- 不填充中心 -->
<div class="nine-slice nine-slice-no-fill [--nine-src:url('/public/images/frame.png')] [--nine-top:20] [--nine-right:20] [--nine-bottom:20] [--nine-left:20] p-8">
不填充中心
</div>
关键点:
- 使用 CSS border-image 实现精确的9宫格切片
- 4个角保持不拉伸边缘和中心区域可配置拉伸或重复
- 支持 fill/unfill 控制中心区域填充
- 兼容所有现代浏览器
*/
@layer utilities {
.H2 {
@apply text-2xl font-bold;
}
.H3 {
@apply text-xl font-bold;
}
.H4 {
@apply text-base font-light;
}
.frame1 {
@apply nine-slice [--nine-src:url('/public/images/Frame1.png')] [--nine-all:10] [--nine-fill:1];
}
.frame-border2 {
@apply absolute inset-0 nine-slice [--nine-src:url('/public/images/frame-border2.png')] [--nine-all:50] [--nine-fill:1];
}
/* 九宫格边框类 - 支持基础到高级功能 */
.nine-slice {
border-style: solid;
width: 100%;
height: 100%;
/* 边框厚度:基于独立的切片值或快速设置 */
border-width: calc(var(--nine-top, var(--nine-all, 24)) * 1px)
calc(var(--nine-right, var(--nine-all, 24)) * 1px)
calc(var(--nine-bottom, var(--nine-all, 24)) * 1px)
calc(var(--nine-left, var(--nine-all, 24)) * 1px);
/* 边框图像 */
border-image-source: var(--nine-src);
/* 边框图像宽度 */
border-image-width: var(--nine-width, 1);
/* 边框图像向外扩展 */
border-image-outset: calc(var(--nine-outset, 0) * 1px);
/* 重复模式 - 支持 stretch, repeat, round, space */
border-image-repeat: var(--nine-repeat, stretch);
/* 默认切片设置(填充中心) */
border-image-slice: var(--nine-top, var(--nine-all, 24)) var(--nine-right, var(--nine-all, 24))
var(--nine-bottom, var(--nine-all, 24)) var(--nine-left, var(--nine-all, 24)) fill;
}
/* 不填充中心的变体 */
.nine-slice.nine-slice-no-fill {
border-image-slice: var(--nine-top, var(--nine-all, 24)) var(--nine-right, var(--nine-all, 24))
var(--nine-bottom, var(--nine-all, 24)) var(--nine-left, var(--nine-all, 24));
}
/* 动画支持 - 平滑过渡 */
.nine-slice-animated {
transition:
border-image-source 0.3s ease,
border-width 0.3s ease,
border-image-slice 0.3s ease;
}
}
/*
大屏看板专用 TailwindCSS Utilities
设计目标:
- 适配4K大屏分辨率 (3840x2160)
- 支持多种布局模式(网格、弹性、绝对定位)
- 提供专业的数据可视化容器
- 优化文字可读性和对比度
- 支持动态主题切换
*/
@layer utilities {
/* 大屏布局容器 */
.dashboard-container {
@apply w-full h-screen overflow-hidden bg-slate-900 text-white;
}
.dashboard-grid {
@apply grid gap-4 p-6 h-full;
}
.dashboard-grid-2x2 {
@apply grid grid-cols-2 grid-rows-2 gap-6 p-8;
}
.dashboard-grid-3x3 {
@apply grid grid-cols-3 grid-rows-3 gap-4 p-6;
}
.dashboard-grid-4x4 {
@apply grid grid-cols-4 grid-rows-4 gap-3 p-4;
}
/* 大屏卡片容器 */
.dashboard-card {
@apply bg-slate-800/80 backdrop-blur-sm rounded-xl border border-slate-700 p-6 shadow-2xl;
}
.dashboard-card-highlight {
@apply dashboard-card border-blue-500/50 bg-slate-800/90;
}
.dashboard-card-warning {
@apply dashboard-card border-amber-500/50 bg-orange-900/20;
}
.dashboard-card-danger {
@apply dashboard-card border-red-500/50 bg-red-900/20;
}
/* 大屏标题文字 */
.dashboard-title {
@apply text-4xl font-bold text-white tracking-wide mb-4;
}
.dashboard-subtitle {
@apply text-2xl font-semibold text-slate-300 mb-3;
}
.dashboard-metric-title {
@apply text-lg font-medium text-slate-400 mb-2;
}
/* 大屏数值显示 */
.dashboard-metric-large {
@apply text-6xl font-bold text-white tabular-nums;
}
.dashboard-metric-medium {
@apply text-5xl font-semibold text-white tabular-nums;
}
.dashboard-metric-small {
@apply text-3xl font-medium text-slate-200 tabular-nums;
}
.dashboard-metric-trend-up {
@apply text-green-400 text-2xl font-semibold;
}
.dashboard-metric-trend-down {
@apply text-red-400 text-2xl font-semibold;
}
/* 图表容器 */
.chart-container {
@apply w-full h-80 bg-slate-800/50 rounded-lg p-4 border border-slate-700;
}
.chart-container-large {
@apply w-full h-96 bg-slate-800/50 rounded-lg p-4 border border-slate-700;
}
.chart-container-small {
@apply w-full h-48 bg-slate-800/50 rounded-lg p-3 border border-slate-700;
}
/* 进度条 */
.dashboard-progress {
@apply w-full h-3 bg-slate-700 rounded-full overflow-hidden;
}
.dashboard-progress-bar {
@apply h-full bg-gradient-to-r from-blue-500 to-cyan-400 rounded-full transition-all duration-500;
}
.dashboard-progress-bar-success {
@apply bg-gradient-to-r from-green-500 to-emerald-400;
}
.dashboard-progress-bar-warning {
@apply bg-gradient-to-r from-amber-500 to-yellow-400;
}
.dashboard-progress-bar-danger {
@apply bg-gradient-to-r from-red-500 to-rose-400;
}
/* 状态指示器 */
.status-indicator {
@apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium;
}
.status-online {
@apply status-indicator bg-green-500/20 text-green-300 border border-green-500/30;
}
.status-offline {
@apply status-indicator bg-red-500/20 text-red-300 border border-red-500/30;
}
.status-warning {
@apply status-indicator bg-amber-500/20 text-amber-300 border border-amber-500/30;
}
.status-dot {
@apply w-3 h-3 rounded-full mr-2;
}
.status-dot-online {
@apply status-dot bg-green-400 animate-pulse;
}
.status-dot-offline {
@apply status-dot bg-red-400;
}
.status-dot-warning {
@apply status-dot bg-amber-400;
}
/* 动画效果 */
.dashboard-fade-in {
@apply animate-fade-in duration-1000;
}
.dashboard-slide-up {
@apply animate-slide-up duration-700;
}
.dashboard-scale-in {
@apply animate-scale-in duration-500;
}
/* 自定义动画 */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-fade-in {
animation: fade-in ease-out;
}
.animate-slide-up {
animation: slide-up ease-out;
}
.animate-scale-in {
animation: scale-in ease-out;
}
/* 数据表格样式 */
.dashboard-table {
@apply w-full border-collapse bg-slate-800/50 rounded-lg overflow-hidden;
}
.dashboard-table th {
@apply px-6 py-4 text-left text-sm font-semibold text-slate-300 border-b border-slate-700 bg-slate-800/80;
}
.dashboard-table td {
@apply px-6 py-4 text-sm text-slate-200 border-b border-slate-700/50;
}
.dashboard-table tr:hover {
@apply bg-slate-700/30;
}
/* 滚动条样式 */
.dashboard-scrollbar {
overflow: auto;
scrollbar-width: thin;
scrollbar-color: #475569 #1e293b;
}
.dashboard-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.dashboard-scrollbar::-webkit-scrollbar-track {
background: #1e293b;
border-radius: 4px;
}
.dashboard-scrollbar::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
.dashboard-scrollbar::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
/* 全屏模式 */
.dashboard-fullscreen {
@apply fixed inset-0 z-50 bg-slate-900;
}
/* 分屏布局 */
.dashboard-split-horizontal {
@apply grid grid-cols-2 gap-0 h-full;
}
.dashboard-split-vertical {
@apply grid grid-rows-2 gap-0 h-full;
}
/* 边缘对齐 */
.dashboard-align-top {
@apply flex items-start justify-center;
}
.dashboard-align-bottom {
@apply flex items-end justify-center;
}
.dashboard-align-left {
@apply flex items-center justify-start;
}
.dashboard-align-right {
@apply flex items-center justify-end;
}
/* 页面过渡动画 - 淡入淡出效果 */
.page-transition {
@apply transition-all duration-1000 ease-out;
}
.page-enter {
@apply opacity-0 scale-95;
}
.page-enter-active {
@apply transition-all duration-1000 ease-out;
}
.page-enter-to {
@apply opacity-100 scale-100;
}
.page-leave {
@apply opacity-100 scale-100;
}
.page-leave-active {
@apply transition-all duration-500 ease-in;
}
.page-leave-to {
@apply opacity-0 scale-95;
}
/* 元素淡入动画 */
.fade-in {
animation: fade-in 1s ease-out forwards;
opacity: 0; /* 初始状态设为透明,避免闪烁 */
}
.fade-in-left {
animation: fade-in-left 1s ease-out forwards;
opacity: 0; /* 初始状态设为透明,避免闪烁 */
}
.fade-in-right {
animation: fade-in-right 1s ease-out forwards;
opacity: 0; /* 初始状态设为透明,避免闪烁 */
}
.fade-in-up {
animation: fade-in-up 1s ease-out forwards;
}
.fade-in-down {
animation: fade-in-down 1s ease-out forwards;
}
/* 淡入动画关键帧 */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-in-left {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fade-in-right {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in-down {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 动画延迟工具类 */
.delay-100 {
animation-delay: 100ms;
}
.delay-200 {
animation-delay: 200ms;
}
.delay-300 {
animation-delay: 300ms;
}
.delay-400 {
animation-delay: 400ms;
}
.delay-500 {
animation-delay: 500ms;
}
.delay-600 {
animation-delay: 600ms;
}
.delay-700 {
animation-delay: 700ms;
}
.delay-800 {
animation-delay: 800ms;
}
.delay-900 {
animation-delay: 900ms;
}
.delay-1000 {
animation-delay: 1000ms;
}
}

View File

@@ -0,0 +1,743 @@
<template>
<div class="universal-query-builder bg-white rounded-lg shadow-lg p-6">
<!-- 表选择 -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2"> 选择表 </label>
<select
v-model="selectedTable"
@change="onTableChange"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">请选择表</option>
<option v-for="table in availableTables" :key="table.key" :value="table.key">
{{ table.name }}
</option>
</select>
</div>
<!-- 查询条件构造器 -->
<div v-if="selectedTable && tableInfo" class="mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">查询条件</h3>
<button
@click="addCondition"
class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
添加条件
</button>
</div>
<!-- 条件列表 -->
<div v-if="queryOptions.where && queryOptions.where.length > 0" class="space-y-4">
<div
v-for="(condition, index) in queryOptions.where"
:key="index"
class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg"
>
<!-- 逻辑操作符 -->
<select
v-if="index > 0"
v-model="condition.logicalOperator"
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="AND">AND</option>
<option value="OR">OR</option>
</select>
<!-- 字段选择 -->
<select
v-model="condition.field"
@change="onFieldChange(condition)"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">选择字段</option>
<option v-for="field in tableInfo.fields" :key="field.name" :value="field.name">
{{ field.name }} ({{ field.type }})
</option>
</select>
<!-- 操作符选择 -->
<select
v-model="condition.operator"
@change="onOperatorChange(condition)"
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">选择操作符</option>
<option
v-for="operator in getAvailableOperators(condition.field)"
:key="operator.value"
:value="operator.value"
>
{{ operator.label }}
</option>
</select>
<!-- 值输入 -->
<div class="flex-1">
<!-- 单值输入 -->
<input
v-if="
!isMultiValueOperator(condition.operator) && !isNullOperator(condition.operator)
"
v-model="condition.value"
:type="getInputType(condition.field, condition.operator)"
:placeholder="getInputPlaceholder(condition.field, condition.operator)"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<!-- 多值输入 -->
<div v-else-if="isMultiValueOperator(condition.operator)" class="space-y-2">
<input
v-if="condition.operator === 'between' || condition.operator === 'date_between'"
v-model="condition.value[0]"
:type="getInputType(condition.field, condition.operator)"
placeholder="起始值"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
v-if="condition.operator === 'between' || condition.operator === 'date_between'"
v-model="condition.value[1]"
:type="getInputType(condition.field, condition.operator)"
placeholder="结束值"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<textarea
v-if="condition.operator === 'in' || condition.operator === 'not_in'"
v-model="condition.valueText"
@input="onMultiValueInput(condition)"
placeholder="输入多个值,用逗号分隔"
rows="2"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<!-- 空值操作符不需要输入 -->
<span v-else-if="isNullOperator(condition.operator)" class="text-gray-500 italic">
无需输入值
</span>
</div>
<!-- 删除按钮 -->
<button
@click="removeCondition(index)"
class="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500"
>
删除
</button>
</div>
</div>
<div v-else class="text-gray-500 text-center py-8">
暂无查询条件,点击"添加条件"开始构建查询
</div>
</div>
<!-- 搜索框 -->
<div v-if="selectedTable && tableInfo" class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2"> 全文搜索 </label>
<input
v-model="queryOptions.search"
type="text"
placeholder="输入搜索关键词..."
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p class="text-sm text-gray-500 mt-1">搜索字段: {{ tableInfo.searchFields.join(', ') }}</p>
</div>
<!-- 排序设置 -->
<div v-if="selectedTable && tableInfo" class="mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">排序设置</h3>
<button
@click="addSortCondition"
class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500"
>
添加排序
</button>
</div>
<div v-if="queryOptions.orderBy && queryOptions.orderBy.length > 0" class="space-y-2">
<div
v-for="(sort, index) in queryOptions.orderBy"
:key="index"
class="flex items-center space-x-4 p-3 bg-gray-50 rounded-lg"
>
<select
v-model="sort.field"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">选择字段</option>
<option v-for="field in tableInfo.sortableFields" :key="field" :value="field">
{{ field }}
</option>
</select>
<select
v-model="sort.direction"
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="ASC">升序</option>
<option value="DESC">降序</option>
</select>
<button
@click="removeSortCondition(index)"
class="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500"
>
删除
</button>
</div>
</div>
</div>
<!-- 分页设置 -->
<div v-if="selectedTable" class="mb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">分页设置</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">页码</label>
<input
v-model.number="queryOptions.pagination.page"
type="number"
min="1"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">每页数量</label>
<select
v-model.number="queryOptions.pagination.limit"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option :value="10">10</option>
<option :value="20">20</option>
<option :value="50">50</option>
<option :value="100">100</option>
</select>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div v-if="selectedTable" class="flex space-x-4">
<button
@click="executeQuery"
:disabled="loading"
class="flex-1 px-6 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
>
{{ loading ? '查询中...' : '执行查询' }}
</button>
<button
@click="executeAggregateQuery"
:disabled="loading"
class="px-6 py-3 bg-purple-500 text-white rounded-md hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:opacity-50"
>
聚合查询
</button>
<button
@click="clearQuery"
class="px-6 py-3 bg-gray-500 text-white rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
清空
</button>
</div>
<!-- 查询结果 -->
<div v-if="queryResult" class="mt-8">
<h3 class="text-lg font-medium text-gray-900 mb-4">查询结果</h3>
<!-- 结果统计 -->
<div class="mb-4 p-4 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-800">
<span v-if="queryResult.pagination">
第 {{ queryResult.pagination.page }} 页,共 {{ queryResult.pagination.totalPages }} 页,
总计 {{ queryResult.total }} 条记录
</span>
<span v-else> 总计 {{ queryResult.total }} 条记录 </span>
</p>
</div>
<!-- 数据表格 -->
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
v-for="field in getDisplayFields()"
:key="field"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{{ field }}
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="(item, index) in queryResult.data" :key="index">
<td
v-for="field in getDisplayFields()"
:key="field"
class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
>
{{ formatValue(item[field]) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 聚合结果 -->
<div v-if="aggregateResult" class="mt-8">
<h3 class="text-lg font-medium text-gray-900 mb-4">聚合结果</h3>
<div class="bg-gray-50 rounded-lg p-4">
<pre class="text-sm">{{ JSON.stringify(aggregateResult, null, 2) }}</pre>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { AdvancedQueryOptions, QueryCondition, SortCondition } from '@nuxt4crud/shared/types'
/**
* 组件属性
* Component props
*/
interface Props {
modelValue?: AdvancedQueryOptions
table?: string
}
/**
* 组件事件
* Component events
*/
interface Emits {
(e: 'update:modelValue', value: AdvancedQueryOptions): void
(e: 'query', result: any): void
(e: 'aggregate', result: any): void
(e: 'search', options: AdvancedQueryOptions): void
(e: 'reset'): void
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => ({
where: [],
orderBy: [],
pagination: { page: 1, limit: 20 },
}),
})
const emit = defineEmits<Emits>()
// 响应式数据
const selectedTable = ref(props.table || '')
const availableTables = ref<any[]>([])
const tableInfo = ref<any>(null)
const loading = ref(false)
const queryResult = ref<any>(null)
const aggregateResult = ref<any>(null)
// 监听table属性变化
watch(() => props.table, (newTable) => {
if (newTable) {
selectedTable.value = newTable
}
})
// 查询选项
const queryOptions = ref<AdvancedQueryOptions>({
where: [],
orderBy: [],
search: '',
pagination: { page: 1, limit: 20 },
})
/**
* 监听查询选项变化
* Watch query options changes
*/
watch(
queryOptions,
newValue => {
emit('update:modelValue', newValue)
},
{ deep: true }
)
/**
* 监听外部传入的值
* Watch external value changes
*/
watch(
() => props.modelValue,
newValue => {
if (newValue && JSON.stringify(newValue) !== JSON.stringify(queryOptions.value)) {
queryOptions.value = { ...newValue }
}
},
{ immediate: true }
)
/**
* 获取可用表列表
* Get available tables
*/
async function loadAvailableTables() {
try {
const response = await $fetch('/api/table-info')
if (response.success) {
availableTables.value = response.data.tables
} else {
// 提供默认的表列表,确保能够正确加载用户表和文章表
availableTables.value = [
{
key: 'User',
name: '用户表'
},
{
key: 'Post',
name: '文章表'
}
]
}
} catch (error) {
console.error('Failed to load tables:', error)
// 错误时提供默认的表列表
availableTables.value = [
{
key: 'User',
name: '用户表'
},
{
key: 'Post',
name: '文章表'
}
]
}
}
/**
* 表选择变化处理
* Handle table selection change
*/
async function onTableChange() {
if (!selectedTable.value) {
tableInfo.value = null
return
}
try {
const response = await $fetch(`/api/table-info?table=${selectedTable.value}`)
if (response.success) {
tableInfo.value = response.data.table
// 清空之前的查询条件
clearQuery()
}
} catch (error) {
console.error('Failed to load table info:', error)
}
}
/**
* 添加查询条件
* Add query condition
*/
function addCondition() {
const condition: QueryCondition & { logicalOperator?: string; valueText?: string } = {
field: '',
operator: 'equals' as any,
value: '',
logicalOperator: queryOptions.value.where!.length > 0 ? 'AND' : undefined,
valueText: '',
}
if (!queryOptions.value.where) {
queryOptions.value.where = []
}
queryOptions.value.where.push(condition as any)
}
/**
* 删除查询条件
* Remove query condition
*/
function removeCondition(index: number) {
if (queryOptions.value.where) {
queryOptions.value.where.splice(index, 1)
}
}
/**
* 字段变化处理
* Handle field change
*/
function onFieldChange(condition: any) {
// 重置操作符和值
condition.operator = 'equals'
condition.value = ''
condition.valueText = ''
}
/**
* 操作符变化处理
* Handle operator change
*/
function onOperatorChange(condition: any) {
// 根据操作符类型初始化值
if (isMultiValueOperator(condition.operator)) {
if (condition.operator === 'between' || condition.operator === 'date_between') {
condition.value = ['', '']
} else {
condition.value = []
condition.valueText = ''
}
} else if (isNullOperator(condition.operator)) {
condition.value = null
} else {
condition.value = ''
}
}
/**
* 多值输入处理
* Handle multi-value input
*/
function onMultiValueInput(condition: any) {
if (condition.valueText) {
condition.value = condition.valueText
.split(',')
.map((v: string) => v.trim())
.filter(Boolean)
} else {
condition.value = []
}
}
/**
* 获取可用操作符
* Get available operators for field
*/
function getAvailableOperators(fieldName: string) {
if (!tableInfo.value || !fieldName) return []
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
if (!field) return []
return tableInfo.value.operators.filter((op: any) => op.types.includes(field.type))
}
/**
* 判断是否为多值操作符
* Check if operator requires multiple values
*/
function isMultiValueOperator(operator: string): boolean {
return ['in', 'not_in', 'between', 'date_between'].includes(operator)
}
/**
* 判断是否为空值操作符
* Check if operator is null operator
*/
function isNullOperator(operator: string): boolean {
return ['is_null', 'is_not_null'].includes(operator)
}
/**
* 获取输入框类型
* Get input type for field and operator
*/
function getInputType(fieldName: string, operator: string): string {
if (!tableInfo.value || !fieldName) return 'text'
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
if (!field) return 'text'
if (operator.startsWith('date_') || field.type === 'datetime') {
return 'datetime-local'
} else if (field.type === 'number') {
return 'number'
}
return 'text'
}
/**
* 获取输入框占位符
* Get input placeholder
*/
function getInputPlaceholder(fieldName: string, operator: string): string {
if (!tableInfo.value || !fieldName) return ''
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
if (!field) return ''
return `请输入${field.name}的值`
}
/**
* 添加排序条件
* Add sort condition
*/
function addSortCondition() {
const sortCondition: SortCondition = {
field: '',
direction: 'ASC',
}
if (!queryOptions.value.orderBy) {
queryOptions.value.orderBy = []
}
queryOptions.value.orderBy.push(sortCondition)
}
/**
* 删除排序条件
* Remove sort condition
*/
function removeSortCondition(index: number) {
if (queryOptions.value.orderBy) {
queryOptions.value.orderBy.splice(index, 1)
}
}
/**
* 执行查询
* Execute query
*/
async function executeQuery() {
if (!selectedTable.value) return
loading.value = true
queryResult.value = null
aggregateResult.value = null
// 发出搜索事件,传递查询选项
emit('search', queryOptions.value)
try {
const response = await $fetch('/api/universal-query', {
method: 'POST',
body: {
table: selectedTable.value,
options: queryOptions.value,
mode: 'query',
},
})
if (response.success) {
queryResult.value = response.data
emit('query', response.data)
}
} catch (error) {
console.error('Query failed:', error)
alert('查询失败: ' + (error as any).data?.message || '未知错误')
} finally {
loading.value = false
}
}
/**
* 执行聚合查询
* Execute aggregate query
*/
async function executeAggregateQuery() {
if (!selectedTable.value) return
loading.value = true
queryResult.value = null
aggregateResult.value = null
try {
// 构建聚合选项
const aggregateOptions = {
...queryOptions.value,
aggregate: {
count: true,
sum: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
avg: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
min: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
max: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
},
}
const response = await $fetch('/api/universal-query', {
method: 'POST',
body: {
table: selectedTable.value,
options: aggregateOptions,
mode: 'aggregate',
},
})
if (response.success) {
aggregateResult.value = response.data.result
emit('aggregate', response.data)
}
} catch (error) {
console.error('Aggregate query failed:', error)
alert('聚合查询失败: ' + (error as any).data?.message || '未知错误')
} finally {
loading.value = false
}
}
/**
* 清空查询
* Clear query
*/
function clearQuery() {
queryOptions.value = {
where: [],
orderBy: tableInfo.value?.defaultOrderBy || [],
search: '',
pagination: { page: 1, limit: 20 },
}
queryResult.value = null
aggregateResult.value = null
// 发出重置事件
emit('reset')
}
/**
* 获取显示字段
* Get display fields
*/
function getDisplayFields(): string[] {
if (!queryResult.value?.data || queryResult.value.data.length === 0) return []
return Object.keys(queryResult.value.data[0])
}
/**
* 格式化值显示
* Format value for display
*/
function formatValue(value: any): string {
if (value === null || value === undefined) return '-'
if (typeof value === 'object') return JSON.stringify(value)
if (typeof value === 'string' && value.includes('T')) {
// 可能是日期时间
try {
return new Date(value).toLocaleString()
} catch {
return value
}
}
return String(value)
}
// 组件挂载时加载表列表
onMounted(() => {
loadAvailableTables()
})
</script>
<style scoped>
.universal-query-builder {
max-width: 100%;
}
</style>

View File

@@ -0,0 +1,25 @@
/**
* PowerStation 模块入口
* 统一导出所有组合式函数和类型定义
*/
// 类型导出
export type {
BreadcrumbItem, HighlightConfig, InteractionCallbacks,
// 交互系统类型
InteractionState, LightingMode,
// 模型管理系统类型
ModelHierarchyNode, ModelModeContext, RaycasterConfig, RotateDirection,
// 视图控制类型
StandardViewType
} from './types'
// 组合式函数导出
export { useInteraction } from './useInteraction'
export { useModelManager } from './useModelManager'
export { useThreeScene } from './useThreeScene'
// 模式导出
export { useFullMode } from './modes/useFullMode'
export { useSimplifiedMode } from './modes/useSimplifiedMode'

View File

@@ -0,0 +1,363 @@
/**
* 完整模式 - 模型加载
* 支持完整的模型层级结构和合并优化
*/
import { ElMessage } from 'element-plus'
import * as THREE from 'three'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { markRaw } from 'vue'
import type { ModelModeContext } from '../types'
// ============================================================
// 配置常量
// ============================================================
/** DRACO解码器路径 */
const DRACO_DECODER_PATH = 'https://www.gstatic.com/draco/v1/decoders/'
// ============================================================
// 辅助函数
// ============================================================
/**
* 从URL提取文件名不含路径和扩展名
* @param url - 文件URL
* @returns 文件名
*/
const extractFileName = (url: string): string => {
const urlParts = url.split('/')
const fileNameWithExt = urlParts[urlParts.length - 1]
const fileName = fileNameWithExt.split('.').slice(0, -1).join('.')
return fileName || '模型'
}
/**
* 递归打印模型中所有物体的路径(用于调试)
* @param object - Three.js 对象
* @param path - 当前路径
*/
const printModelObjectPaths = (object: THREE.Object3D, path: string = '') => {
const fullPath = path ? `${path}/${object.name}` : object.name
object.children.forEach(child => {
printModelObjectPaths(child, fullPath)
})
}
// ============================================================
// 几何体合并
// ============================================================
/**
* 合并子树中的所有网格
* 用于优化渲染性能
* @param root - 要合并的根对象
*/
const mergeSubTree = (root: THREE.Object3D) => {
// 收集所有网格
const meshes: THREE.Mesh[] = []
root.traverse(child => {
if ((child as THREE.Mesh).isMesh) {
meshes.push(child as THREE.Mesh)
}
})
// 检查是否需要合并
if (meshes.length === 0) return
if (meshes.length === 1 && root === meshes[0] && root.children.length === 0) return
const geometries: THREE.BufferGeometry[] = []
const materials: THREE.Material[] = []
const materialIndexMap = new Map<string, number>()
// 更新世界矩阵
root.updateMatrixWorld(true)
const rootInverse = root.matrixWorld.clone().invert()
// 检查UV属性兼容性
const hasUVAttribute = meshes.some(mesh => mesh.geometry.attributes.uv)
const allHaveUV = meshes.every(mesh => mesh.geometry.attributes.uv)
if (hasUVAttribute && !allHaveUV) {
console.warn(`节点 ${root.name} 的几何体UV属性不一致将移除UV属性以确保合并成功`)
}
// 处理每个网格
meshes.forEach(mesh => {
const geom = mesh.geometry.clone()
mesh.updateMatrixWorld(true)
const matrix = mesh.matrixWorld.clone().premultiply(rootInverse)
geom.applyMatrix4(matrix)
// 重新计算法线
if (geom.attributes.normal) {
delete geom.attributes.normal
}
geom.computeVertexNormals()
// 处理UV属性兼容性
if (hasUVAttribute && !allHaveUV && geom.attributes.uv) {
delete geom.attributes.uv
}
const meshMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
// 处理几何体组
if (!geom.groups || geom.groups.length === 0) {
geom.clearGroups()
if (geom.attributes.position) {
geom.addGroup(0, geom.attributes.position.count, 0)
}
}
// 重新映射材质索引
const newGroups = []
for (const group of geom.groups) {
const originalMat = meshMaterials[group.materialIndex || 0]
if (!originalMat) continue
const matUuid = originalMat.uuid
let newMatIndex = materialIndexMap.get(matUuid)
if (newMatIndex === undefined) {
newMatIndex = materials.length
materials.push(originalMat)
materialIndexMap.set(matUuid, newMatIndex)
}
newGroups.push({
start: group.start,
count: group.count,
materialIndex: newMatIndex,
})
}
geom.groups = newGroups
geometries.push(geom)
})
// 执行合并
if (geometries.length > 0) {
try {
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries, true)
// 重新计算法线
if (mergedGeometry.attributes.normal) {
mergedGeometry.computeVertexNormals()
}
const mergedMesh = new THREE.Mesh(mergedGeometry, materials)
mergedMesh.name = root.name
mergedMesh.userData = { ...root.userData }
// 替换原对象
const parent = root.parent
if (parent) {
mergedMesh.position.copy(root.position)
mergedMesh.rotation.copy(root.rotation)
mergedMesh.scale.copy(root.scale)
parent.remove(root)
parent.add(mergedMesh)
}
} catch (e) {
console.warn('合并节点失败:', root.name, e)
}
}
}
// ============================================================
// 主钩子函数
// ============================================================
/**
* 完整模式组合式函数
* @param context - 模型模式上下文
*/
export function useFullMode(context: ModelModeContext) {
const {
store,
isLoading,
archetypeModel,
modelGroup,
objectMap,
modelCache,
replaceWithPBRMaterial,
generateHierarchy,
initEnvironment,
applyLightingMode,
fitView,
findNodeById,
rebuildObjectMap,
generateBreadcrumbs,
} = context
// ============================================================
// 模型加载
// ============================================================
/**
* 加载模型
* @param url - 模型URL
* @param onLoaded - 加载完成回调
*/
const loadModel = (url: string, onLoaded?: () => void) => {
isLoading.value = true
store.isModelLoading = true
const fileName = extractFileName(url)
// 检查缓存
if (store.modelCacheEnabled && modelCache.has(fileName)) {
console.log('从缓存加载模型:', fileName)
const cachedModel = modelCache.get(fileName)
// 使用缓存的模型
const clonedModel = cachedModel!.clone()
replaceWithPBRMaterial(clonedModel)
archetypeModel.value = markRaw(clonedModel)
// 生成层级结构
const rootPath = store.currentNodeId
const hierarchy = generateHierarchy(archetypeModel.value, rootPath)
store.modelHierarchy = [hierarchy]
initEnvironment()
applyLightingMode()
isLoading.value = false
store.isModelLoading = false
if (onLoaded) onLoaded()
return
}
// 创建加载器
const loader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath(DRACO_DECODER_PATH)
loader.setDRACOLoader(dracoLoader)
loader.load(
url,
gltf => {
replaceWithPBRMaterial(gltf.scene)
// 缓存模型
if (store.modelCacheEnabled) {
modelCache.set(fileName, gltf.scene.clone())
console.log('模型已缓存:', fileName)
}
// 设置原型模型
archetypeModel.value = markRaw(gltf.scene)
// 生成层级结构
const rootPath = store.currentNodeId
const hierarchy = generateHierarchy(archetypeModel.value, rootPath)
store.modelHierarchy = [hierarchy]
initEnvironment()
applyLightingMode()
isLoading.value = false
store.isModelLoading = false
dracoLoader.dispose()
if (onLoaded) onLoaded()
},
undefined,
error => {
console.error('模型加载错误:', error)
ElMessage.error('模型加载失败,请检查网络连接或模型文件是否存在')
isLoading.value = false
store.isModelLoading = false
dracoLoader.dispose()
}
)
}
// ============================================================
// 模型处理
// ============================================================
/**
* 处理模型(核心逻辑)
* 合并子节点并更新场景
* @param nodeId - 节点ID
* @param shouldFitView - 是否适配视图
*/
const processModel = (nodeId: string, shouldFitView: boolean = false) => {
if (!archetypeModel.value) return
// 克隆模型
const newModel = archetypeModel.value.clone()
newModel.name = store.currentNodeId || '模型'
// 查找目标节点
let targetNode = findNodeById(newModel, nodeId)
if (!targetNode) {
console.warn(`节点 ${nodeId} 未找到,回退到根节点`)
targetNode = newModel
}
// 合并子节点
const childrenToMerge = [...targetNode.children]
childrenToMerge.forEach(child => {
mergeSubTree(child)
})
// 清空模型组
modelGroup.clear()
// 计算世界变换
newModel.updateMatrixWorld(true)
const worldPosition = new THREE.Vector3()
const worldQuaternion = new THREE.Quaternion()
const worldScale = new THREE.Vector3()
targetNode.getWorldPosition(worldPosition)
targetNode.getWorldQuaternion(worldQuaternion)
targetNode.getWorldScale(worldScale)
// 添加到模型组
modelGroup.add(targetNode)
// 应用世界变换
targetNode.position.copy(worldPosition)
targetNode.quaternion.copy(worldQuaternion)
targetNode.scale.copy(worldScale)
// 重建物体映射
objectMap.clear()
rebuildObjectMap(targetNode)
// 更新层级结构
const newHierarchy = generateHierarchy(targetNode, targetNode.userData.id)
store.modelHierarchy = [newHierarchy]
// 生成面包屑(返回供调用者处理)
const breadcrumbs = generateBreadcrumbs(nodeId)
// 适配视图
if (shouldFitView) {
fitView(targetNode)
}
// 调试:打印物体路径
printModelObjectPaths(newModel)
}
// ============================================================
// 导出接口
// ============================================================
return {
/** 加载状态(只读) */
isLoading: readonly(isLoading),
/** 加载模型 */
loadModel,
/** 处理模型 */
processModel,
}
}

View File

@@ -0,0 +1,219 @@
/**
* 简化模式 - 模型加载
* 按文件名直接加载模型,支持按需加载和自动回退
*/
import { ElMessage } from 'element-plus'
import type * as THREE from 'three'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { markRaw } from 'vue'
import type { ModelModeContext } from '../types'
// ============================================================
// 配置常量
// ============================================================
/** DRACO解码器路径 */
const DRACO_DECODER_PATH = 'https://www.gstatic.com/draco/v1/decoders/'
/** 简化模式配置 */
const SIMPLIFIED_MODE_CONFIG = {
/** 层级显示的最大深度 */
maxHierarchyDepth: 2,
/** 模型文件扩展名 */
fileExtension: '.glb',
} as const
// ============================================================
// 主钩子函数
// ============================================================
/**
* 简化模式组合式函数
* @param context - 模型模式上下文
*/
export function useSimplifiedMode(context: ModelModeContext) {
const {
store,
isLoading,
archetypeModel,
modelGroup,
objectMap,
modelCache,
replaceWithPBRMaterial,
generateHierarchy,
initEnvironment,
applyLightingMode,
fitView,
rebuildObjectMap,
generateBreadcrumbs,
} = context
// ============================================================
// 模型设置
// ============================================================
/**
* 从缓存设置模型
* @param model - 缓存的模型
* @param fileName - 文件名
*/
const setupModelFromCache = (model: THREE.Object3D, fileName: string) => {
const clonedModel = model.clone()
const nameWithoutExt = fileName.replace(SIMPLIFIED_MODE_CONFIG.fileExtension, '')
// 设置模型名称
clonedModel.name = nameWithoutExt
// 更新原型模型
archetypeModel.value = markRaw(clonedModel.clone())
// 清空并添加到模型组
modelGroup.clear()
modelGroup.add(clonedModel)
// 生成层级结构(只显示指定深度的节点)
let hierarchy = []
if (clonedModel.children.length > 0) {
// 如果模型有子节点,将每个子节点作为一级节点
clonedModel.children.forEach((child, index) => {
const childPath = index === 0 ? nameWithoutExt : `${nameWithoutExt}~child${index}`
const childHierarchy = generateHierarchy(
child,
childPath,
1,
SIMPLIFIED_MODE_CONFIG.maxHierarchyDepth
)
hierarchy.push(childHierarchy)
})
} else {
// 如果模型没有子节点,使用模型本身作为一级节点
const modelHierarchy = generateHierarchy(
clonedModel,
nameWithoutExt,
1,
SIMPLIFIED_MODE_CONFIG.maxHierarchyDepth
)
hierarchy = [modelHierarchy]
}
store.modelHierarchy = hierarchy
// 生成面包屑(返回供调用者处理)
const breadcrumbs = generateBreadcrumbs(nameWithoutExt)
// 重建物体映射
objectMap.clear()
rebuildObjectMap(clonedModel)
// 初始化环境和光照
initEnvironment()
applyLightingMode()
// 适配视图
fitView(clonedModel)
}
// ============================================================
// 模型加载
// ============================================================
/**
* 根据命名约定构建模型URL
* @param fileName - 文件名
* @returns 完整URL路径
*/
const buildModelUrl = (fileName: string): string => {
// 格式: Root~Parent~Child.glb -> /3DModels/Root/Root~Parent~Child.glb
const nameWithoutExt = fileName.slice(0, -4)
const rootFolder = nameWithoutExt.split('~')[0]
return `/3DModels/${rootFolder}/${fileName}`
}
/**
* 按文件名加载模型
* @param fileName - 文件名(可带或不带.glb扩展名
* @param onLoaded - 加载完成回调
*/
const loadModelByFileName = (fileName: string, onLoaded?: () => void) => {
// 确保有.glb扩展名
if (!fileName.endsWith(SIMPLIFIED_MODE_CONFIG.fileExtension)) {
fileName += SIMPLIFIED_MODE_CONFIG.fileExtension
}
// 检查缓存
if (store.modelCacheEnabled && modelCache.has(fileName)) {
const cached = modelCache.get(fileName)!
setupModelFromCache(cached, fileName)
if (onLoaded) onLoaded()
return
}
// 开始加载
isLoading.value = true
store.isModelLoading = true
const url = buildModelUrl(fileName)
console.log(`${url} 加载模型`)
// 创建加载器
const loader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath(DRACO_DECODER_PATH)
loader.setDRACOLoader(dracoLoader)
loader.load(
url,
gltf => {
const rawModel = gltf.scene
replaceWithPBRMaterial(rawModel)
// 缓存模型
if (store.modelCacheEnabled) {
modelCache.set(fileName, rawModel.clone())
}
// 设置模型
setupModelFromCache(rawModel, fileName)
isLoading.value = false
store.isModelLoading = false
dracoLoader.dispose()
if (onLoaded) onLoaded()
},
undefined,
error => {
console.warn('加载模型失败:', fileName, error)
ElMessage.error(`模型加载失败: ${fileName},请检查网络连接或模型文件是否存在`)
// 回退逻辑:尝试加载父级模型
const nameWithoutExt = fileName.slice(0, -4)
const parts = nameWithoutExt.split('~')
if (parts.length > 1) {
// 移除最后一级,尝试加载父级
parts.pop()
const parentName = parts.join('~')
console.log('回退到父级:', parentName)
loadModelByFileName(parentName, onLoaded)
} else {
// 没有父级可回退
console.error('没有父级可以回退,或根节点加载失败。')
isLoading.value = false
store.isModelLoading = false
dracoLoader.dispose()
}
}
)
}
// ============================================================
// 导出接口
// ============================================================
return {
/** 按文件名加载模型 */
loadModelByFileName,
}
}

View File

@@ -0,0 +1,156 @@
/**
* PowerStation 模块的共享类型定义
* 用于 Three.js 场景、模型管理和交互系统
*/
import type * as THREE from 'three'
import type { ShallowRef } from 'vue'
// ============================================================
// 交互系统类型
// ============================================================
/**
* 交互状态
*/
export interface InteractionState {
/** 当前悬停的对象 */
hoveredObject: THREE.Object3D | null
/** 当前选中的对象 */
selectedObject: THREE.Object3D | null
/** 是否正在拖拽 */
isDragging: boolean
/** 拖拽起始位置 */
dragStart: THREE.Vector2
/** 拖拽阈值 */
dragThreshold: number
}
/**
* 交互回调函数
*/
export interface InteractionCallbacks {
onHover?: (object: THREE.Object3D | null) => void
onSelect?: (object: THREE.Object3D | null) => void
onDoubleClick?: (object: THREE.Object3D) => void
onDragStart?: (object: THREE.Object3D, point: THREE.Vector3) => void
onDragMove?: (object: THREE.Object3D, point: THREE.Vector3) => void
onDragEnd?: (object: THREE.Object3D) => void
}
/**
* 高亮配置
*/
export interface HighlightConfig {
/** 高亮颜色 */
color: THREE.Color
/** 高亮强度 */
intensity: number
/** 动画持续时间(毫秒) */
duration: number
}
/**
* 射线检测配置
*/
export interface RaycasterConfig {
/** 是否递归检测子节点 */
recursive: boolean
/** 检测阈值 */
threshold: number
}
// ============================================================
// 模型管理系统类型
// ============================================================
/**
* 模型层级节点
*/
export interface ModelHierarchyNode {
/** 节点ID */
id: string
/** 节点标签 */
label: string
/** 子节点 */
children: ModelHierarchyNode[]
}
/**
* 面包屑导航项
*/
export interface BreadcrumbItem {
/** 显示文本 */
text: string
/** 跳转链接 */
to?: string
}
/**
* 模型加载模式上下文
* 用于在不同加载模式间共享数据和方法
*/
export interface ModelModeContext {
/** 状态存储 */
store: any
/** 加载状态 */
isLoading: globalThis.Ref<boolean>
/** 原型模型 */
archetypeModel: ShallowRef<THREE.Object3D | null>
/** 模型组 */
modelGroup: THREE.Group
/** 物体ID映射 */
objectMap: Map<string, THREE.Object3D>
/** 模型缓存 */
modelCache: Map<string, THREE.Object3D>
// 方法
/** 替换为PBR材质 */
replaceWithPBRMaterial: (object: THREE.Object3D) => void
/** 生成层级结构 */
generateHierarchy: (
object: THREE.Object3D,
currentPath: string,
depth?: number,
maxDepth?: number
) => ModelHierarchyNode
/** 初始化环境 */
initEnvironment: () => void
/** 应用光照模式 */
applyLightingMode: () => void
/** 适配视图 */
fitView: (targetObj?: THREE.Object3D, keepOrientation?: boolean) => void
/** 通过ID查找节点 */
findNodeById: (root: THREE.Object3D, id: string) => THREE.Object3D | null
/** 重建物体映射 */
rebuildObjectMap: (root: THREE.Object3D) => void
/** 生成面包屑 */
generateBreadcrumbs: (nodeId: string) => BreadcrumbItem[]
}
// ============================================================
// 视图控制类型
// ============================================================
/**
* 标准视图类型
*/
export type StandardViewType = 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso'
/**
* 视图旋转方向
*/
export type RotateDirection =
| 'up'
| 'down'
| 'left'
| 'right'
| 'upleft'
| 'upright'
| 'downleft'
| 'downright'
/**
* 光照模式
*/
export type LightingMode = 'basic' | 'advanced'

View File

@@ -0,0 +1,408 @@
/**
* 三维场景交互系统
* 处理鼠标悬停、点击、双击等交互事件
* 提供物体高亮和轮廓效果
*/
import { navigateTo } from 'nuxt/app'
import * as THREE from 'three'
import type { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
import { useRoute } from 'vue-router'
import { usePowerStationStore } from '~/stores/powerStation'
import type { HighlightConfig, RaycasterConfig } from './types'
// ============================================================
// 配置常量
// ============================================================
/** 悬停轮廓颜色 */
const HOVER_OUTLINE_COLOR = '#435c9d'
/** 点击轮廓颜色 */
const CLICK_OUTLINE_COLOR = '#9d7e43'
/** 轮廓通道配置 */
const OUTLINE_PASS_CONFIG = {
edgeStrength: 2,
edgeGlow: 1,
edgeThickness: 1,
} as const
/** 默认高亮配置 */
const DEFAULT_HIGHLIGHT_CONFIG: HighlightConfig = {
color: new THREE.Color(0x00ff00),
intensity: 0.5,
duration: 200,
}
/** 默认射线检测配置 */
const DEFAULT_RAYCASTER_CONFIG: RaycasterConfig = {
recursive: true,
threshold: 0.1,
}
// ============================================================
// 材质管理
// ============================================================
/** 存储原始材质用于恢复 */
const originalMaterials = new Map<string, THREE.Material | THREE.Material[]>()
/**
* 高亮物体(使用线框材质)
* @param object - 要高亮的物体
* @param config - 高亮配置
*/
const highlightObject = (
object: THREE.Object3D,
config: HighlightConfig = DEFAULT_HIGHLIGHT_CONFIG
) => {
object.traverse(child => {
if (child instanceof THREE.Mesh) {
const uuid = child.uuid
// 存储原始材质
if (!originalMaterials.has(uuid)) {
originalMaterials.set(uuid, child.material)
}
// 创建高亮材质
const highlightMaterial = new THREE.MeshBasicMaterial({
color: config.color,
transparent: true,
opacity: config.intensity,
wireframe: true,
})
child.material = highlightMaterial
}
})
}
/**
* 取消高亮物体(恢复原始材质)
* @param object - 要取消高亮的物体
*/
const unhighlightObject = (object: THREE.Object3D) => {
object.traverse(child => {
if (child instanceof THREE.Mesh) {
const uuid = child.uuid
const originalMaterial = originalMaterials.get(uuid)
if (originalMaterial) {
child.material = originalMaterial
originalMaterials.delete(uuid)
}
}
})
}
// ============================================================
// 主交互钩子
// ============================================================
/**
* 交互系统组合式函数
* @param getScene - 获取场景
* @param getCamera - 获取相机
* @param getComposer - 获取后期处理合成器
* @param getContainer - 获取容器元素
* @param modelGroup - 模型组
* @param getArchetypeModel - 获取原型模型
*/
export function useInteraction(
getScene: () => THREE.Scene,
getCamera: () => THREE.PerspectiveCamera,
getComposer: () => EffectComposer,
getContainer: () => HTMLElement | undefined,
modelGroup: THREE.Group,
getArchetypeModel: () => THREE.Object3D | null
) {
const store = usePowerStationStore()
const route = useRoute()
// 射线检测器
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
// 轮廓通道
let hoverOutlinePass: OutlinePass
let clickOutlinePass: OutlinePass
// 当前交互状态
let currentHoverObject: THREE.Object3D | null = null
let currentClickObject: THREE.Object3D | null = null
// ============================================================
// 射线检测
// ============================================================
/**
* 获取鼠标与场景物体的交点
* @param event - 鼠标事件
* @returns 交点数组
*/
const getIntersects = (event: MouseEvent): THREE.Intersection[] => {
const container = getContainer()
const camera = getCamera()
if (!container || !camera) return []
const rect = container.getBoundingClientRect()
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, camera)
return raycaster.intersectObjects(modelGroup.children, true)
}
/**
* 查找具有用户数据ID的父级物体
* @param object - 起始物体
* @returns 具有ID的物体或null
*/
const findObjectWithId = (object: THREE.Object3D): THREE.Object3D | null => {
let target = object
while (target && !target.userData.id && target !== modelGroup) {
if (target.parent) {
target = target.parent
} else {
break
}
}
return target?.userData.id ? target : null
}
// ============================================================
// 高亮效果
// ============================================================
/**
* 高亮悬停物体
* @param object - 要高亮的物体null清除高亮
*/
const highlightHover = (object: THREE.Object3D | null) => {
if (!hoverOutlinePass) return
if (object && object !== currentClickObject) {
if (currentHoverObject !== object) {
hoverOutlinePass.selectedObjects = [object]
currentHoverObject = object
}
} else {
hoverOutlinePass.selectedObjects = []
currentHoverObject = null
}
}
/**
* 高亮点击物体
* @param object - 要高亮的物体null清除高亮
*/
const highlightClick = (object: THREE.Object3D | null) => {
if (!clickOutlinePass) return
if (object) {
// 清除相同物体的悬停高亮
if (currentHoverObject === object) {
hoverOutlinePass.selectedObjects = []
currentHoverObject = null
}
clickOutlinePass.selectedObjects = [object]
currentClickObject = object
} else {
clickOutlinePass.selectedObjects = []
currentClickObject = null
}
}
// ============================================================
// 事件处理
// ============================================================
/**
* 点击事件处理
*/
const onClick = (event: MouseEvent) => {
const intersects = getIntersects(event)
if (intersects.length > 0 && intersects[0]) {
const target = findObjectWithId(intersects[0].object)
if (target && target.userData.id) {
const id = target.userData.id
const currentRouteNodeId = route.query.currentNodeId as string
const shouldHighlight = id !== currentRouteNodeId
// 更新store中的选中节点
store.selectNode({ id, label: target.name || `节点 ${id}` })
// 只有ID不一致时才高亮
if (shouldHighlight) {
highlightClick(target)
} else {
highlightClick(null)
}
}
} else {
// 点击空白区域,恢复到路由参数值
const currentRouteNodeId = route.query.currentNodeId as string
if (currentRouteNodeId) {
store.selectNode({ id: currentRouteNodeId, label: currentRouteNodeId })
} else {
store.selectNode({ id: '' })
}
highlightClick(null)
}
}
/**
* 双击事件处理
*/
const onDoubleClick = async (event: MouseEvent) => {
const intersects = getIntersects(event)
if (intersects.length > 0 && intersects[0]) {
const target = findObjectWithId(intersects[0].object)
if (target && target.userData.id) {
const id = target.userData.id
await navigateTo({
query: {
...route.query,
currentNodeId: id,
},
})
}
} else {
// 双击空白区域,返回父级
await handleNavigateToParent()
}
}
/**
* 导航到父级节点
*/
const handleNavigateToParent = async () => {
const currentRouteNodeId = route.query.currentNodeId as string
// 如果是根节点,不操作
if (!currentRouteNodeId || !currentRouteNodeId.includes('~')) {
return
}
// 删除最后一个"~"之后的内容
const lastUnderlineIndex = currentRouteNodeId.lastIndexOf('~')
if (lastUnderlineIndex > 0) {
const newCurrentNodeId = currentRouteNodeId.substring(0, lastUnderlineIndex)
await navigateTo({
query: {
...route.query,
currentNodeId: newCurrentNodeId,
},
})
store.selectNode({ id: newCurrentNodeId, label: newCurrentNodeId })
}
}
/**
* 鼠标移动事件处理
*/
const onMouseMove = (event: MouseEvent) => {
const intersects = getIntersects(event)
if (intersects.length > 0 && intersects[0]) {
const target = findObjectWithId(intersects[0].object)
highlightHover(target)
} else {
highlightHover(null)
}
}
// ============================================================
// 初始化与清理
// ============================================================
/**
* 初始化交互系统
*/
const initInteraction = () => {
const scene = getScene()
const camera = getCamera()
const composer = getComposer()
const container = getContainer()
if (!scene || !camera || !composer || !container) return
const width = container.clientWidth
const height = container.clientHeight
// 创建悬停轮廓通道
hoverOutlinePass = new OutlinePass(new THREE.Vector2(width, height), scene, camera)
hoverOutlinePass.edgeStrength = OUTLINE_PASS_CONFIG.edgeStrength
hoverOutlinePass.edgeGlow = OUTLINE_PASS_CONFIG.edgeGlow
hoverOutlinePass.edgeThickness = OUTLINE_PASS_CONFIG.edgeThickness
hoverOutlinePass.visibleEdgeColor.set(HOVER_OUTLINE_COLOR)
hoverOutlinePass.hiddenEdgeColor.set(HOVER_OUTLINE_COLOR)
composer.addPass(hoverOutlinePass)
// 创建点击轮廓通道
clickOutlinePass = new OutlinePass(new THREE.Vector2(width, height), scene, camera)
clickOutlinePass.edgeStrength = OUTLINE_PASS_CONFIG.edgeStrength
clickOutlinePass.edgeGlow = OUTLINE_PASS_CONFIG.edgeGlow
clickOutlinePass.edgeThickness = OUTLINE_PASS_CONFIG.edgeThickness
clickOutlinePass.visibleEdgeColor.set(CLICK_OUTLINE_COLOR)
clickOutlinePass.hiddenEdgeColor.set(CLICK_OUTLINE_COLOR)
composer.addPass(clickOutlinePass)
// 绑定事件
container.addEventListener('click', onClick)
container.addEventListener('dblclick', onDoubleClick)
container.addEventListener('mousemove', onMouseMove)
}
/**
* 更新轮廓通道尺寸
* @param width - 新宽度
* @param height - 新高度
*/
const updatePassSize = (width: number, height: number) => {
if (hoverOutlinePass) hoverOutlinePass.setSize(width, height)
if (clickOutlinePass) clickOutlinePass.setSize(width, height)
}
/**
* 清理资源
*/
const dispose = () => {
const container = getContainer()
if (container) {
container.removeEventListener('click', onClick)
container.removeEventListener('dblclick', onDoubleClick)
container.removeEventListener('mousemove', onMouseMove)
}
}
// ============================================================
// 导出接口
// ============================================================
return {
/** 初始化交互系统 */
initInteraction,
/** 高亮点击物体 */
highlightClick,
/** 高亮悬停物体 */
highlightHover,
/** 更新轮廓通道尺寸 */
updatePassSize,
/** 清理资源 */
dispose,
}
}

View File

@@ -0,0 +1,551 @@
/**
* 模型管理系统
* 处理3D模型的加载、层级结构生成、材质处理和相机适配
*/
import * as TWEEN from '@tweenjs/tween.js'
import * as THREE from 'three'
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
import { usePowerStationStore } from '~/stores/powerStation'
import { useFullMode } from './modes/useFullMode'
import { useSimplifiedMode } from './modes/useSimplifiedMode'
import type { BreadcrumbItem, LightingMode, ModelHierarchyNode } from './types'
// ============================================================
// 配置常量
// ============================================================
/** 光源配置 */
const LIGHT_CONFIG = {
/** 环境光颜色 */
ambientColor: 0xffffff,
/** 环境光强度 */
ambientIntensity: 0.6,
/** 方向光颜色 */
directionalColor: 0xffffff,
/** 方向光强度 */
directionalIntensity: 1,
/** 方向光位置 */
directionalPosition: new THREE.Vector3(5, 3, 5),
} as const
/** PBR材质配置 */
const PBR_MATERIAL_CONFIG = {
/** 金属度 */
metalness: 0.6,
/** 粗糙度 */
roughness: 0.6,
/** 环境贴图强度 */
envMapIntensity: 1.5,
} as const
/** 相机动画配置 */
const CAMERA_ANIMATION_CONFIG = {
/** 动画时长(毫秒) */
duration: 900,
/** 距离系数 */
distanceMultiplier: 1.2,
} as const
// ============================================================
// 主钩子函数
// ============================================================
/**
* 模型管理组合式函数
* @param getScene - 获取场景
* @param getCamera - 获取相机
* @param getControls - 获取控制器
* @param getRenderer - 获取渲染器
*/
export function useModelManager(
getScene: () => THREE.Scene,
getCamera: () => THREE.PerspectiveCamera,
getControls: () => any, // OrbitControls
getRenderer: () => THREE.WebGLRenderer
) {
const store = usePowerStationStore()
const isLoading = ref(true)
// ============================================================
// 模型数据
// ============================================================
// 使用 markRaw 标记 Three.js 对象为非响应式,防止 Vue DevTools 追踪导致性能问题
const modelGroup = markRaw(new THREE.Group())
const archetypeModel = shallowRef<THREE.Object3D | null>(null)
// 使用普通 MapThree.js 对象已是 markRawMap 本身不需要响应式)
const objectMap = new Map<string, THREE.Object3D>()
const modelCache = new Map<string, THREE.Object3D>()
// 环境贴图
let envTexture: THREE.Texture | null = null
// 光照模式
const lightingMode = ref<LightingMode>('basic')
// ============================================================
// 初始化
// ============================================================
/**
* 初始化模型系统
* 添加模型组和基础光源到场景
*/
const initModelSystem = () => {
const scene = getScene()
if (!scene) return
// 添加模型组
scene.add(modelGroup)
// 添加基础光源
const ambientLight = new THREE.AmbientLight(
LIGHT_CONFIG.ambientColor,
LIGHT_CONFIG.ambientIntensity
)
scene.add(ambientLight)
const dirLight = new THREE.DirectionalLight(
LIGHT_CONFIG.directionalColor,
LIGHT_CONFIG.directionalIntensity
)
dirLight.position.copy(LIGHT_CONFIG.directionalPosition)
scene.add(dirLight)
}
/**
* 初始化环境贴图
*/
const initEnvironment = () => {
const renderer = getRenderer()
const scene = getScene()
if (!renderer || !scene) return
// 避免重复创建
if (envTexture) return
const pmremGenerator = new THREE.PMREMGenerator(renderer)
pmremGenerator.compileEquirectangularShader()
const roomEnvironment = new RoomEnvironment()
envTexture = pmremGenerator.fromScene(roomEnvironment).texture
if (lightingMode.value === 'advanced') {
scene.environment = envTexture
}
// 清理临时对象
roomEnvironment.dispose()
pmremGenerator.dispose()
}
// ============================================================
// 层级结构生成
// ============================================================
/**
* 生成模型层级结构
* @param object - Three.js 对象
* @param currentPath - 当前路径
* @param depth - 当前深度
* @param maxDepth - 最大深度
* @returns 层级节点
*/
const generateHierarchy = (
object: THREE.Object3D,
currentPath: string,
depth: number = 1,
maxDepth: number = Infinity
): ModelHierarchyNode => {
const isRoot = object === archetypeModel.value
const nodeId = isRoot ? store.currentNodeId || currentPath : currentPath
// 设置 userData.id
object.userData.id = nodeId
const node: ModelHierarchyNode = {
id: nodeId,
label: isRoot
? store.currentNodeId || object.name || `节点 ${nodeId}`
: object.name || `节点 ${nodeId}`,
children: [],
}
// 检查是否为完整物体有名称且包含多个子Mesh
const isCompleteObject =
object.name && object.children.filter(child => child.type === 'Mesh').length > 1
// 达到最大深度则停止递归
if (depth >= maxDepth) {
return node
}
// 递归处理子节点
object.children.forEach(child => {
if (child.type === 'Mesh' || child.type === 'Group' || child.type === 'Object3D') {
const childName = child.name || '子对象'
const childPath = `${currentPath}~${childName}`
// 跳过完整物体中的无名Mesh子节点
if (isCompleteObject && child.type === 'Mesh' && !child.name) {
return
}
const childNode = generateHierarchy(child, childPath, depth + 1, maxDepth)
node.children.push(childNode)
}
})
return node
}
// ============================================================
// 节点查找
// ============================================================
/**
* 通过ID查找节点
* @param root - 根对象
* @param id - 节点ID
* @returns 找到的节点或null
*/
const findNodeById = (root: THREE.Object3D, id: string): THREE.Object3D | null => {
if (root.userData.id === id) return root
for (const child of root.children) {
const found = findNodeById(child, id)
if (found) return found
}
return null
}
/**
* 重建物体映射表
* @param root - 根对象
*/
const rebuildObjectMap = (root: THREE.Object3D) => {
if (root.userData.id) {
objectMap.set(root.userData.id, root)
}
root.children.forEach(child => rebuildObjectMap(child))
}
// ============================================================
// 面包屑生成
// ============================================================
/**
* 生成面包屑导航数据
* @param nodeId - 节点ID
* @returns 面包屑数组
*/
const generateBreadcrumbs = (nodeId: string): BreadcrumbItem[] => {
if (!archetypeModel.value) return []
const breadcrumbsList: BreadcrumbItem[] = []
const nodeIdParts = nodeId.split('~')
if (nodeIdParts.length > 0) {
let currentPath = ''
for (let i = 0; i < nodeIdParts.length; i++) {
const part = nodeIdParts[i]
currentPath = i === 0 ? part : `${currentPath}~${part}`
const currentObject = findNodeById(archetypeModel.value, currentPath)
const displayText = currentObject
? currentObject.name
: part || findNodeById(archetypeModel.value, currentPath)?.name || part
const item: BreadcrumbItem = { text: displayText }
if (currentPath !== nodeId) {
item.to = `?currentNodeId=${currentPath}`
}
breadcrumbsList.push(item)
}
}
// 使用当前节点ID的第一部分作为面包屑首项
if (breadcrumbsList.length > 0 && store.currentNodeId) {
const firstPart = store.currentNodeId.split('~')[0]
breadcrumbsList[0].text = firstPart
}
return breadcrumbsList
}
// ============================================================
// 相机适配
// ============================================================
/**
* 适配视图到目标对象
* @param targetObj - 目标对象默认为modelGroup
* @param keepOrientation - 是否保持当前相机朝向
*/
const fitView = (targetObj?: THREE.Object3D, keepOrientation: boolean = true) => {
const obj = targetObj || modelGroup
const camera = getCamera()
const controls = getControls()
console.log('[fitView] 开始执行:', {
hasTargetObj: !!targetObj,
objName: obj?.name,
objChildrenCount: obj?.children?.length,
hasCamera: !!camera,
hasControls: !!controls,
})
if (!obj || !camera || !controls) {
console.warn('[fitView] 提前返回: obj/camera/controls 缺失')
return
}
// 计算包围盒
const box = new THREE.Box3().setFromObject(obj)
console.log('[fitView] 包围盒:', {
isEmpty: box.isEmpty(),
min: box.min.toArray(),
max: box.max.toArray(),
})
if (box.isEmpty()) {
console.warn('[fitView] 提前返回: 包围盒为空')
return
}
const size = box.getSize(new THREE.Vector3())
const center = box.getCenter(new THREE.Vector3())
// 计算适配距离
const maxSize = Math.max(size.x, size.y, size.z)
const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camera.fov) / 360))
const fitWidthDistance = fitHeightDistance / camera.aspect
const distance = CAMERA_ANIMATION_CONFIG.distanceMultiplier * Math.max(fitHeightDistance, fitWidthDistance)
// 计算相机方向
let direction: THREE.Vector3
if (keepOrientation) {
direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize()
if (direction.lengthSq() < 0.0001) {
direction = new THREE.Vector3(1, 1, 1).normalize()
}
} else {
direction = new THREE.Vector3(1, 1, 1).normalize()
}
const targetPosition = center.clone().add(direction.multiplyScalar(distance))
console.log('[fitView] 动画参数:', {
size: size.toArray(),
center: center.toArray(),
distance,
currentCameraPos: camera.position.toArray(),
targetCameraPos: targetPosition.toArray(),
})
// 相机位置动画
new TWEEN.Tween(camera.position)
.to({ x: targetPosition.x, y: targetPosition.y, z: targetPosition.z }, CAMERA_ANIMATION_CONFIG.duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onStart(() => console.log('[fitView] 相机位置动画开始'))
.onComplete(() => console.log('[fitView] 相机位置动画完成'))
.start()
// 控制目标动画
new TWEEN.Tween(controls.target)
.to({ x: center.x, y: center.y, z: center.z }, CAMERA_ANIMATION_CONFIG.duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onStart(() => console.log('[fitView] 控制目标动画开始'))
.onUpdate(() => {
controls.update()
})
.onComplete(() => console.log('[fitView] 控制目标动画完成'))
.start()
}
// ============================================================
// 材质处理
// ============================================================
/**
* 替换为PBR材质
* @param object - 要处理的对象
*/
const replaceWithPBRMaterial = (object: THREE.Object3D) => {
object.traverse(child => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
const newMaterials: THREE.MeshStandardMaterial[] = []
materials.forEach(mat => {
const color =
(mat as THREE.MeshBasicMaterial).color instanceof THREE.Color
? (mat as THREE.MeshBasicMaterial).color.clone()
: new THREE.Color(0xaaaaaa)
// 获取原始贴图
const originalMap = (mat as THREE.MeshBasicMaterial).map || null
const alphaMap = originalMap || null
const useAlpha = originalMap !== null
const pbrMaterial = new THREE.MeshStandardMaterial({
color: color,
metalness: PBR_MATERIAL_CONFIG.metalness,
roughness: PBR_MATERIAL_CONFIG.roughness,
transparent: useAlpha || ((mat as THREE.MeshBasicMaterial).transparent ?? false),
opacity: useAlpha ? 1 : ((mat as THREE.MeshBasicMaterial).opacity ?? 1),
side: mat.side || THREE.FrontSide,
map: originalMap,
alphaMap: alphaMap,
normalMap: (mat as THREE.MeshStandardMaterial).normalMap || null,
envMapIntensity: PBR_MATERIAL_CONFIG.envMapIntensity,
})
newMaterials.push(pbrMaterial)
})
if (newMaterials.length > 0) {
mesh.material = (Array.isArray(mesh.material) ? newMaterials : newMaterials[0]) as
| THREE.Material
| THREE.Material[]
}
}
})
}
/**
* 更新材质模式
* @param root - 根对象
* @param mode - 光照模式
*/
const updateMaterials = (root: THREE.Object3D, mode: LightingMode) => {
root.traverse(child => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
const newMaterials = materials.map(mat => {
if (mode === 'basic') {
// 切换到基础模式
if (mat.type === 'MeshLambertMaterial') return mat
if (mat.userData.lambertPartner) return mat.userData.lambertPartner
const source = mat as THREE.MeshStandardMaterial
const lambert = new THREE.MeshLambertMaterial({
color: source.color,
map: source.map,
transparent: source.transparent,
opacity: source.opacity,
side: source.side,
alphaMap: source.alphaMap,
aoMap: source.aoMap,
})
mat.userData.lambertPartner = lambert
lambert.userData.pbrPartner = mat
return lambert
} else {
// 切换到高级模式
if (mat.type === 'MeshStandardMaterial') return mat
if (mat.userData.pbrPartner) return mat.userData.pbrPartner
return mat
}
})
mesh.material = Array.isArray(mesh.material) ? newMaterials : newMaterials[0]
}
})
}
// ============================================================
// 光照模式控制
// ============================================================
/**
* 切换光照模式
*/
const toggleLighting = () => {
lightingMode.value = lightingMode.value === 'basic' ? 'advanced' : 'basic'
applyLightingMode()
}
/**
* 应用当前光照模式
*/
const applyLightingMode = () => {
const scene = getScene()
if (!scene) return
if (lightingMode.value === 'advanced') {
if (envTexture) scene.environment = envTexture
} else {
scene.environment = null
}
if (modelGroup) updateMaterials(modelGroup, lightingMode.value)
if (archetypeModel.value) updateMaterials(archetypeModel.value, lightingMode.value)
}
// ============================================================
// 模式上下文
// ============================================================
/** 供加载模式使用的共享上下文 */
const context = {
store,
isLoading,
archetypeModel,
modelGroup,
objectMap,
modelCache,
replaceWithPBRMaterial,
generateHierarchy,
initEnvironment,
applyLightingMode,
fitView,
findNodeById,
rebuildObjectMap,
generateBreadcrumbs,
}
// 实例化加载模式
const { loadModel, processModel } = useFullMode(context)
const { loadModelByFileName } = useSimplifiedMode(context)
// ============================================================
// 导出接口
// ============================================================
return {
// 数据
modelGroup,
isLoading,
lightingMode,
objectMap,
archetypeModel,
// 初始化
initModelSystem,
// 模型加载
loadModel,
processModel,
loadModelByFileName,
// 视图控制
fitView,
// 光照控制
toggleLighting,
// 工具函数
findNodeById,
generateBreadcrumbs,
}
}

View File

@@ -0,0 +1,469 @@
/**
* Three.js 场景管理系统
* 提供场景、相机、渲染器、控制器的初始化和管理
* 支持投影模式切换、标准视图和视角旋转
*/
import * as TWEEN from '@tweenjs/tween.js'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import type { RotateDirection, StandardViewType } from './types'
// ============================================================
// 配置常量
// ============================================================
/** 相机配置 */
const CAMERA_CONFIG = {
/** 透视相机视角 */
fov: 45,
/** 近裁剪面 */
near: 0.01,
/** 远裁剪面 */
far: 10000,
/** 初始位置 */
position: new THREE.Vector3(10, 10, 10),
} as const
/** 正交相机视锥体大小 */
const ORTHOGRAPHIC_FRUSTUM_SIZE = 20
/** 渲染器配置 */
const RENDERER_CONFIG = {
/** 抗锯齿 */
antialias: true,
/** 透明背景 */
alpha: true,
/** 阴影映射 */
shadowMap: false,
/** 色调映射 */
toneMapping: THREE.ACESFilmicToneMapping,
/** 曝光度 */
toneMappingExposure: 1.0,
} as const
/** 控制器配置 */
const CONTROLS_CONFIG = {
/** 阻尼效果 */
enableDamping: true,
/** 阻尼系数 */
dampingFactor: 0.05,
/** 允许缩放 */
enableZoom: true,
/** 允许平移 */
enablePan: true,
} as const
/** 动画配置 */
const ANIMATION_CONFIG = {
/** 视图切换动画时长(毫秒) */
viewTransitionDuration: 800,
/** 旋转动画时长(毫秒) */
rotationDuration: 500,
/** 旋转角度(弧度) */
rotateAngle: Math.PI / 8, // 22.5度
} as const
/** 场景背景色 */
const SCENE_BACKGROUND_COLOR = 0x1a1a1a
// ============================================================
// 主钩子函数
// ============================================================
/**
* Three.js 场景组合式函数
* 管理Three.js的完整生命周期
*/
export function useThreeScene() {
// 容器引用
const containerRef = ref<HTMLElement>()
// Three.js 核心实例
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let composer: EffectComposer
let animationId: number
let resizeObserver: ResizeObserver
let renderPass: RenderPass
// 响应式状态
const isOrthographic = ref(false)
// 回调钩子
const onAnimateCallbacks: (() => void)[] = []
const onResizeCallbacks: (() => void)[] = []
// ============================================================
// 初始化
// ============================================================
/**
* 初始化Three.js场景
*/
const initThree = () => {
if (!containerRef.value) return
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
// 创建场景
scene = new THREE.Scene()
scene.background = new THREE.Color(SCENE_BACKGROUND_COLOR)
// 创建透视相机
camera = new THREE.PerspectiveCamera(
CAMERA_CONFIG.fov,
width / height,
CAMERA_CONFIG.near,
CAMERA_CONFIG.far
)
camera.position.copy(CAMERA_CONFIG.position)
// 创建渲染器
renderer = new THREE.WebGLRenderer({
antialias: RENDERER_CONFIG.antialias,
alpha: RENDERER_CONFIG.alpha,
})
renderer.setSize(width, height)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = RENDERER_CONFIG.shadowMap
renderer.toneMapping = RENDERER_CONFIG.toneMapping
renderer.toneMappingExposure = RENDERER_CONFIG.toneMappingExposure
renderer.outputColorSpace = THREE.SRGBColorSpace
containerRef.value.appendChild(renderer.domElement)
// 创建控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = CONTROLS_CONFIG.enableDamping
controls.dampingFactor = CONTROLS_CONFIG.dampingFactor
controls.enableZoom = CONTROLS_CONFIG.enableZoom
controls.enablePan = CONTROLS_CONFIG.enablePan
// 创建后期处理合成器
composer = new EffectComposer(renderer)
composer.setPixelRatio(window.devicePixelRatio)
renderPass = new RenderPass(scene, camera)
composer.addPass(renderPass)
// 创建尺寸观察器
resizeObserver = new ResizeObserver(() => onWindowResize())
resizeObserver.observe(containerRef.value)
// 启动渲染循环
animate()
}
// ============================================================
// 渲染循环
// ============================================================
/**
* 渲染循环
*/
const animate = () => {
animationId = requestAnimationFrame(animate)
// 更新TWEEN动画
TWEEN.update()
// 更新控制器
if (controls) controls.update()
// 执行外部回调
onAnimateCallbacks.forEach(cb => cb())
// 渲染
if (composer) {
composer.render()
} else if (renderer && scene && camera) {
renderer.render(scene, camera)
}
}
// ============================================================
// 尺寸调整
// ============================================================
/**
* 窗口尺寸变化处理
*/
const onWindowResize = () => {
if (!containerRef.value || !camera || !renderer || !composer) return
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
// 根据相机类型更新投影矩阵
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = width / height
camera.updateProjectionMatrix()
} else if (camera instanceof THREE.OrthographicCamera) {
const aspect = width / height
camera.left = (-ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2
camera.right = (ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2
camera.top = ORTHOGRAPHIC_FRUSTUM_SIZE / 2
camera.bottom = -ORTHOGRAPHIC_FRUSTUM_SIZE / 2
camera.updateProjectionMatrix()
}
// 更新渲染器和合成器尺寸
renderer.setSize(width, height)
composer.setSize(width, height)
composer.setPixelRatio(window.devicePixelRatio)
// 执行外部回调
onResizeCallbacks.forEach(cb => cb())
}
// ============================================================
// 投影模式切换
// ============================================================
/**
* 切换投影模式(透视/正交)
*/
const toggleProjection = () => {
if (!containerRef.value || !camera || !controls) return
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
const aspect = width / height
// 保存当前状态
const target = controls.target.clone()
const position = camera.position.clone()
if (isOrthographic.value) {
// 切换到透视相机
camera = new THREE.PerspectiveCamera(
CAMERA_CONFIG.fov,
aspect,
CAMERA_CONFIG.near,
CAMERA_CONFIG.far
)
camera.position.copy(position)
} else {
// 切换到正交相机
camera = new THREE.OrthographicCamera(
(-ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2,
(ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2,
ORTHOGRAPHIC_FRUSTUM_SIZE / 2,
-ORTHOGRAPHIC_FRUSTUM_SIZE / 2,
CAMERA_CONFIG.near,
CAMERA_CONFIG.far
)
camera.position.copy(position)
camera.zoom = 1
}
camera.lookAt(target)
// 更新控制器和渲染通道
controls.object = camera
controls.update()
if (renderPass) {
renderPass.camera = camera
}
isOrthographic.value = !isOrthographic.value
onWindowResize() // 确保投影矩阵正确
}
// ============================================================
// 视图控制
// ============================================================
/**
* 设置标准视图
* @param view - 视图类型
*/
const setStandardView = (view: StandardViewType) => {
if (!camera || !controls) return
const target = controls.target.clone()
const distance = camera.position.distanceTo(target)
const newPosition = target.clone()
// 根据视图类型计算新位置
switch (view) {
case 'top':
newPosition.y += distance
break
case 'bottom':
newPosition.y -= distance
break
case 'front':
newPosition.z += distance
break
case 'back':
newPosition.z -= distance
break
case 'left':
newPosition.x -= distance
break
case 'right':
newPosition.x += distance
break
case 'iso': {
const isoDist = distance / Math.sqrt(3)
newPosition.set(target.x + isoDist, target.y + isoDist, target.z + isoDist)
break
}
}
// 动画过渡
new TWEEN.Tween(camera.position)
.to(newPosition, ANIMATION_CONFIG.viewTransitionDuration)
.easing(TWEEN.Easing.Cubic.Out)
.onUpdate(() => {
camera.lookAt(target)
controls.update()
})
.start()
}
/**
* 旋转视图
* @param direction - 旋转方向
*/
const rotateView = (direction: RotateDirection) => {
if (!controls) return
const offset = new THREE.Vector3().subVectors(camera.position, controls.target)
const spherical = new THREE.Spherical().setFromVector3(offset)
const rotateAngle = ANIMATION_CONFIG.rotateAngle
// 根据方向调整球坐标
switch (direction) {
case 'left':
spherical.theta -= rotateAngle
break
case 'right':
spherical.theta += rotateAngle
break
case 'up':
spherical.phi -= rotateAngle
break
case 'down':
spherical.phi += rotateAngle
break
case 'upleft':
spherical.theta -= rotateAngle
spherical.phi -= rotateAngle
break
case 'upright':
spherical.theta += rotateAngle
spherical.phi -= rotateAngle
break
case 'downleft':
spherical.theta -= rotateAngle
spherical.phi += rotateAngle
break
case 'downright':
spherical.theta += rotateAngle
spherical.phi += rotateAngle
break
}
// 限制phi范围避免翻转
spherical.phi = Math.max(0.01, Math.min(Math.PI - 0.01, spherical.phi))
const newPosition = new THREE.Vector3().setFromSpherical(spherical).add(controls.target)
// 动画过渡
new TWEEN.Tween(camera.position)
.to(newPosition, ANIMATION_CONFIG.rotationDuration)
.easing(TWEEN.Easing.Cubic.Out)
.onUpdate(() => {
camera.lookAt(controls.target)
controls.update()
})
.start()
}
// ============================================================
// 回调注册
// ============================================================
/**
* 添加渲染循环回调
* @param cb - 回调函数
*/
const addAnimateCallback = (cb: () => void) => {
onAnimateCallbacks.push(cb)
}
/**
* 添加尺寸变化回调
* @param cb - 回调函数
*/
const addResizeCallback = (cb: () => void) => {
onResizeCallbacks.push(cb)
}
// ============================================================
// 清理
// ============================================================
/**
* 清理资源
*/
const dispose = () => {
cancelAnimationFrame(animationId)
if (resizeObserver) resizeObserver.disconnect()
if (renderer) {
renderer.dispose()
if (containerRef.value && renderer.domElement) {
containerRef.value.removeChild(renderer.domElement)
}
}
if (composer) composer.dispose()
}
// 组件卸载时清理
onUnmounted(() => {
dispose()
})
// ============================================================
// 导出接口
// ============================================================
return {
// 引用
containerRef,
// 初始化
initThree,
// 获取器
getScene: () => scene,
getCamera: () => camera,
getRenderer: () => renderer,
getControls: () => controls,
getComposer: () => composer,
// 回调注册
addAnimateCallback,
addResizeCallback,
// 尺寸调整
onWindowResize,
// 投影控制
isOrthographic,
toggleProjection,
// 视图控制
setStandardView,
rotateView,
}
}

5
app/eslint.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt()
// Your custom configs here

156
app/nuxt.config.ts Normal file
View File

@@ -0,0 +1,156 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
import { config } from 'dotenv'
// 加载环境变量
config({ path: '../.env' })
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: false },
// 开发服务器配置 - 允许外网访问
devServer: {
host: '0.0.0.0', // 改为0.0.0.0以允许外网访问
port: process.env.NUXT_DEV_PORT || 8080,
},
// 禁用默认布局,确保每个页面使用独立布局
app: {
layoutTransition: false,
pageTransition: false,
},
// 启用 SSR 以获得更好的构建结果
ssr: false,
// 路径别名配置 - 使用 pnpm workspace 包引用
alias: {
'@nuxt4crud/shared': '@nuxt4crud/shared',
},
// Vite 配置
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
'element-plus': ['element-plus'],
'vue-vendor': ['vue'],
},
},
},
// 确保以 ES 模块模式构建
commonjsOptions: {
transformMixedEsModules: true,
},
},
// 解决 CSS 中 import.meta 的问题
css: {
devSourcemap: true,
// 确保 CSS 预处理器正确工作
preprocessorOptions: {
scss: {
api: 'modern-compiler', // 使用现代编译器 API
},
},
},
// 配置 ES 模块处理
esbuild: {
target: 'es2020',
},
optimizeDeps: {
esbuildOptions: {
target: 'es2020',
},
},
},
// 构建优化
build: {
transpile: ['element-plus/es'],
},
modules: [
'@nuxt/eslint',
'@nuxt/image',
'@element-plus/nuxt',
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
],
// Pinia配置
pinia: {
autoImports: [
// 自动导入defineStore和storeToRefs
'defineStore',
'storeToRefs',
],
},
// 指定 TailwindCSS 的全局样式入口文件
tailwindcss: {
cssPath: '~/assets/css/main.css',
},
// PostCSS 配置 - 替代 postcss.config.js
postcss: {
plugins: {
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
},
},
elementPlus: {
/** Options */
// 自动导入样式
importStyle: 'scss',
// 启用组件和指令缓存(仅在开发模式下有效)
cache: true,
// 手动配置需要安装的方法
installMethods: ['ElLoading', 'ElMessage', 'ElMessageBox', 'ElNotification'],
// 全局配置
config: {
size: 'default',
zIndex: 3000,
},
},
// API 代理配置移除Express代理使用Nitro内置API
nitro: {
scanDirs: ['../server/api', '../server/routes'],
srcDir: '../server',
unstorage: {
driver: 'fs',
base: '../.data/db',
},
output: {
serverDir: '.output/server',
},
ignore: ['../scripts/**'],
prerender: {
crawlLinks: false,
routes: [],
},
},
// 运行时配置
runtimeConfig: {
// 私有配置,只在服务端可用
feishuAppId: process.env.FEISHU_APP_ID || 'cli_a62aaf3d9d385013',
feishuAppSecret: process.env.FEISHU_APP_SECRET || 'VH46dJI4T4bbUD7rI9J6Se2EI5rWVpBf',
cozeApiToken:
process.env.COZE_API_TOKEN ||
'pat_7VvJA4an8IbsjmID9SasNBZyVDCJ3NamxwBOBdDxfUQEnGOYGaQf3odBUoibO0aF',
cozeClientId: process.env.COZE_CLIENT_ID || '27608009841040583568491680329534.app.coze',
cozeClientSecret:
process.env.COZE_CLIENT_SECRET || 'Bx7ctHOQlzWVdoaQnSYJgRPwlK64IUuhi4oPnRO3OyJ5K4yc',
// 公共配置,客户端和服务端都可用
public: {
// 更新API基础路径不再指向Express服务器
apiBase: process.env.NUXT_PUBLIC_API_BASE || '',
cozeDefaultWorkflowId: process.env.COZE_DEFAULT_WORKFLOW_ID || '7554970254934605859',
},
},
})

60
app/package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "@nuxt4crud/app",
"version": "1.0.0",
"type": "module",
"private": true,
"packageManager": "pnpm@8",
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.0.0"
},
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"type-check": "nuxt typecheck",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@babylonjs/core": "8.36.1",
"@babylonjs/gui": "^8.36.1",
"@babylonjs/gui-editor": "^8.36.1",
"@babylonjs/inspector": "^8.36.1",
"@babylonjs/loaders": "8.36.1",
"@babylonjs/serializers": "8.36.1",
"@coze/api": "^1.3.8",
"@element-plus/icons-vue": "^2.3.2",
"@element-plus/nuxt": "^1.1.4",
"@kjgl77/datav-vue3": "^1.7.4",
"@nuxt/eslint": "1.10.0",
"@nuxt/fonts": "0.11.4",
"@nuxt/icon": "2.1.0",
"@nuxt/image": "1.11.0",
"@nuxt4crud/shared": "file:../shared",
"poseidon-engine-core": "file:./supreium_viewer/package",
"@nuxtjs/tailwindcss": "^6.14.0",
"@tweenjs/tween.js": "^18.6.4",
"@types/three": "^0.151.0",
"chart.js": "^4.5.1",
"echarts": "^6.0.0",
"element-plus": "^2.11.7",
"eslint": "^9.39.0",
"nuxt": "^4.2.1",
"three": "^0.151.3",
"valibot": "^1.1.0",
"vue": "^3.5.22",
"vue-chartjs": "^5.3.3",
"vue-echarts": "^8.0.1"
},
"devDependencies": {
"@prisma/client": "^6.19.0",
"dotenv": "^17.2.3",
"magicast": "^0.3.5",
"sass-embedded": "^1.93.3",
"tsx": "^4.20.6",
"vite-tsconfig-paths": "^5.1.4"
}
}

20
app/pages/index.vue Normal file
View File

@@ -0,0 +1,20 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-bold text-gray-800 mb-4">Nuxt4CRUD 应用</h1>
<p class="text-lg text-gray-600 mb-8">Prisma 6.x 升级成功</p>
<div class="space-x-4">
<NuxtLink to="/posts" class="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
查看帖子
</NuxtLink>
<NuxtLink to="/dashboard-demo" class="inline-block px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
仪表板演示
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup>
// 首页组件
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

BIN
app/public/assets/ball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
app/public/assets/cross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572456155" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47431" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M141.84448 228.97664a25.6 25.6 0 0 0-8.6528 35.15392l330.61376 546.51392a56.32 56.32 0 0 0 19.03616 19.03616c26.61376 16.1024 61.24032 7.5776 77.34272-19.03616l330.61376-546.51392A25.6 25.6 0 0 0 868.8896 225.28H155.09504a25.6 25.6 0 0 0-13.25056 3.69664zM868.8896 174.08c42.4192 0 76.8 34.38592 76.8 76.8a76.8 76.8 0 0 1-11.0848 39.75168l-330.61376 546.51392c-30.73536 50.81088-96.83968 67.08224-147.65056 36.34688a107.52 107.52 0 0 1-36.34176-36.34688L89.38496 290.6368c-21.95456-36.3008-10.33216-83.51232 25.9584-105.472A76.8 76.8 0 0 1 155.09504 174.08h713.79456z" fill="#bfbfbf" p-id="47432"></path></svg>

After

Width:  |  Height:  |  Size: 988 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572420938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46589" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M795.02336 141.85472a25.6 25.6 0 0 0-35.15392-8.6528L213.35552 463.81568a56.32 56.32 0 0 0-19.03616 19.03616c-16.1024 26.61376-7.5776 61.24032 19.03616 77.34272l546.51392 330.60864a25.6 25.6 0 0 0 38.85056-21.90336V155.10528a25.6 25.6 0 0 0-3.69664-13.25056zM849.92 868.89984c0 42.4192-34.38592 76.8-76.8 76.8a76.8 76.8 0 0 1-39.75168-11.0848L186.8544 604.00128c-50.81088-30.73536-67.08224-96.8448-36.34688-147.65056a107.52 107.52 0 0 1 36.34688-36.34176l546.5088-330.61376c36.3008-21.95456 83.51232-10.33216 105.472 25.9584A76.8 76.8 0 0 1 849.92 155.10528v713.79456z" fill="#bfbfbf" p-id="46590"></path></svg>

After

Width:  |  Height:  |  Size: 989 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572433880" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46874" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M228.97664 141.85472A25.6 25.6 0 0 0 225.28 155.10528v713.79456a25.6 25.6 0 0 0 38.85056 21.90336l546.51392-330.60864c26.61376-16.1024 35.13856-50.72896 19.03616-77.34272a56.32 56.32 0 0 0-19.03616-19.03616L264.13056 133.20192a25.6 25.6 0 0 0-35.15392 8.6528zM174.08 868.89984V155.10528a76.8 76.8 0 0 1 11.08992-39.75168c21.95456-36.29056 69.1712-47.91296 105.46176-25.9584l546.51392 330.61376a107.52 107.52 0 0 1 36.34688 36.34176c30.73536 50.80576 14.464 116.9152-36.34688 147.65056l-546.5088 330.61376A76.8 76.8 0 0 1 250.88 945.69984c-42.41408 0-76.8-34.3808-76.8-76.8z" fill="#bfbfbf" p-id="46875"></path></svg>

After

Width:  |  Height:  |  Size: 994 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572448124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47179" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M882.12992 795.01824a25.6 25.6 0 0 0 8.6528-35.15392L560.16896 213.34528a56.32 56.32 0 0 0-19.03616-19.03616c-26.61376-16.09728-61.2352-7.5776-77.3376 19.03616L133.18144 759.86432a25.6 25.6 0 0 0 21.90336 38.85056h713.79456a25.6 25.6 0 0 0 13.25056-3.69664zM155.0848 849.91488c-42.41408 0-76.8-34.38592-76.8-76.8a76.8 76.8 0 0 1 11.08992-39.75168L419.98848 186.84416c30.73536-50.80576 96.83968-67.07712 147.64544-36.34176a107.52 107.52 0 0 1 36.34688 36.34176l330.61376 546.51904c21.95456 36.29056 10.32704 83.5072-25.9584 105.46176a76.8 76.8 0 0 1-39.7568 11.08992H155.0848z" fill="#bfbfbf" p-id="47180"></path></svg>

After

Width:  |  Height:  |  Size: 996 B

254754
app/public/example.json Normal file

File diff suppressed because it is too large Load Diff

BIN
app/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
app/public/icons/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

1
app/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

48
app/start-all-simple.bat Normal file
View File

@@ -0,0 +1,48 @@
@echo off
REM Nuxt Production Server Startup Script (BAT Version)
REM Author: AI Assistant
REM Version: 1.0
REM Change to script directory
cd /d "%~dp0"
echo =======================================
echo Nuxt4Curd Production Server Startup
echo =======================================
echo.
REM 1. Check Node.js environment
echo 1. Checking Node.js environment...
where node >nul 2>nul
if %ERRORLEVEL% NEQ 0 (
echo ERROR: Node.js is not installed!
echo Please install Node.js v18+ and try again.
pause
exit /b 1
)
REM Get Node.js version
for /f "tokens=*" %%i in ('node --version') do set NODE_VERSION=%%i
echo SUCCESS: Node.js version: %NODE_VERSION%
echo.
REM 2. Check current directory
echo 2. Checking current directory...
if not exist "server\index.mjs" (
echo ERROR: Server files not found!
echo Please ensure server files are in the output directory.
pause
exit /b 1
)
echo SUCCESS: Server files found in current directory.
echo.
REM 3. Start server
echo 3. Starting Nuxt production server...
echo.
echo Press Ctrl+C to stop the server at any time.
echo.
REM Run server command
node server/index.mjs

View File

@@ -0,0 +1,48 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const usePowerStationStore = defineStore('powerStation', () => {
const currentNodeId = ref<string>('')
const modelHierarchy = ref<any[]>([])
const isModelLoading = ref(false)
const loadMode = ref<boolean>(false) // false: 分批加载, true: 完整加载
const modelCacheEnabled = ref<boolean>(false) // false: 不使用缓存, true: 使用缓存
// 聚焦事件监听器
const focusListeners = ref<((nodeId: string) => void)[]>([])
// 操作方法 - 仅处理节点选择
const selectNode = (node: any) => {
currentNodeId.value = node.id
}
// 触发聚焦事件
const triggerFocus = (nodeId: string) => {
focusListeners.value.forEach(listener => listener(nodeId))
}
// 添加聚焦监听器
const addFocusListener = (listener: (nodeId: string) => void) => {
focusListeners.value.push(listener)
}
// 移除聚焦监听器
const removeFocusListener = (listener: (nodeId: string) => void) => {
const index = focusListeners.value.indexOf(listener)
if (index > -1) {
focusListeners.value.splice(index, 1)
}
}
return {
currentNodeId,
modelHierarchy,
isModelLoading,
loadMode,
modelCacheEnabled,
selectNode,
triggerFocus,
addFocusListener,
removeFocusListener,
}
})

View File

@@ -0,0 +1,22 @@
# poseidon-engine-core
This is the core of rendering engine. shared by all rendering project.
use can use submodule to reference this project,
or use package created by poseidon-engine-core-wrap project.
## Breaking Changes
1. 2022-12-29. Avoid export classes as default export, user need to change the way of import.
2. 2022-12-29. to run in node, user need to import 'canvas-gl' and invoke init() function to register the font for canvas before create NodeWebGLRenderer.
3. 2022-12-20. import the classes from 'poseidon-engine-core'
4. 2022-12-20. rename file MeshLineExMaterial, rename MeLineColorMapMaterialParameters to LineColorMapMaterialParameters
5. 2023-1-28. move after-projection-matrix from materials into cameras. do not need to update shaders any more.
6. 2023-2-9. support color mapped materials. use ColorMapExMaterials instead. TODO: support filters later.
## Version History
### v0.2.0
1. 修复ScreenHelper.focusCamera连续多次调用的bug
2. 修改Legend拼写错误

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572456155" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47431" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M141.84448 228.97664a25.6 25.6 0 0 0-8.6528 35.15392l330.61376 546.51392a56.32 56.32 0 0 0 19.03616 19.03616c26.61376 16.1024 61.24032 7.5776 77.34272-19.03616l330.61376-546.51392A25.6 25.6 0 0 0 868.8896 225.28H155.09504a25.6 25.6 0 0 0-13.25056 3.69664zM868.8896 174.08c42.4192 0 76.8 34.38592 76.8 76.8a76.8 76.8 0 0 1-11.0848 39.75168l-330.61376 546.51392c-30.73536 50.81088-96.83968 67.08224-147.65056 36.34688a107.52 107.52 0 0 1-36.34176-36.34688L89.38496 290.6368c-21.95456-36.3008-10.33216-83.51232 25.9584-105.472A76.8 76.8 0 0 1 155.09504 174.08h713.79456z" fill="#bfbfbf" p-id="47432"></path></svg>

After

Width:  |  Height:  |  Size: 988 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572420938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46589" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M795.02336 141.85472a25.6 25.6 0 0 0-35.15392-8.6528L213.35552 463.81568a56.32 56.32 0 0 0-19.03616 19.03616c-16.1024 26.61376-7.5776 61.24032 19.03616 77.34272l546.51392 330.60864a25.6 25.6 0 0 0 38.85056-21.90336V155.10528a25.6 25.6 0 0 0-3.69664-13.25056zM849.92 868.89984c0 42.4192-34.38592 76.8-76.8 76.8a76.8 76.8 0 0 1-39.75168-11.0848L186.8544 604.00128c-50.81088-30.73536-67.08224-96.8448-36.34688-147.65056a107.52 107.52 0 0 1 36.34688-36.34176l546.5088-330.61376c36.3008-21.95456 83.51232-10.33216 105.472 25.9584A76.8 76.8 0 0 1 849.92 155.10528v713.79456z" fill="#bfbfbf" p-id="46590"></path></svg>

After

Width:  |  Height:  |  Size: 989 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572433880" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46874" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M228.97664 141.85472A25.6 25.6 0 0 0 225.28 155.10528v713.79456a25.6 25.6 0 0 0 38.85056 21.90336l546.51392-330.60864c26.61376-16.1024 35.13856-50.72896 19.03616-77.34272a56.32 56.32 0 0 0-19.03616-19.03616L264.13056 133.20192a25.6 25.6 0 0 0-35.15392 8.6528zM174.08 868.89984V155.10528a76.8 76.8 0 0 1 11.08992-39.75168c21.95456-36.29056 69.1712-47.91296 105.46176-25.9584l546.51392 330.61376a107.52 107.52 0 0 1 36.34688 36.34176c30.73536 50.80576 14.464 116.9152-36.34688 147.65056l-546.5088 330.61376A76.8 76.8 0 0 1 250.88 945.69984c-42.41408 0-76.8-34.3808-76.8-76.8z" fill="#bfbfbf" p-id="46875"></path></svg>

After

Width:  |  Height:  |  Size: 994 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572448124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47179" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M882.12992 795.01824a25.6 25.6 0 0 0 8.6528-35.15392L560.16896 213.34528a56.32 56.32 0 0 0-19.03616-19.03616c-26.61376-16.09728-61.2352-7.5776-77.3376 19.03616L133.18144 759.86432a25.6 25.6 0 0 0 21.90336 38.85056h713.79456a25.6 25.6 0 0 0 13.25056-3.69664zM155.0848 849.91488c-42.41408 0-76.8-34.38592-76.8-76.8a76.8 76.8 0 0 1 11.08992-39.75168L419.98848 186.84416c30.73536-50.80576 96.83968-67.07712 147.64544-36.34176a107.52 107.52 0 0 1 36.34688 36.34176l330.61376 546.51904c21.95456 36.29056 10.32704 83.5072-25.9584 105.46176a76.8 76.8 0 0 1-39.7568 11.08992H155.0848z" fill="#bfbfbf" p-id="47180"></path></svg>

After

Width:  |  Height:  |  Size: 996 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export * from './src';

View File

@@ -0,0 +1,16 @@
import { BRepBody, IBRepCompObj } from './BRepBody';
export interface IAssemblyProps {
name: string;
}
export interface IAssembly {
subAssemblies: IAssembly[];
bodies: BRepBody[];
props?: IAssemblyProps;
}
export declare class BRepAssembly implements IAssembly {
readonly subAssemblies: IAssembly[];
readonly bodies: BRepBody[];
props?: IAssemblyProps;
name(): string | undefined;
static fromCompound(comObj: IBRepCompObj): BRepAssembly | null;
}

View File

@@ -0,0 +1,127 @@
import { SupBox } from '../types';
import { BRepEdge } from './BRepEdge';
import { BRepFace } from './BRepFace';
import { BRepObject, IBRepProps } from './BRepObject';
import { BRepShell } from './BRepShell';
import { BRepSolid } from './BRepSolid';
import { BodyType, ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
import { BRepWire } from './BRepWire';
/**
* @brief BRep body基类
*
*/
export declare abstract class BRepBody {
/**
* @brief body类型
*
* @return
* -<em>body 类型</em>
*/
abstract bodyType(): BodyType;
/**
* @brief 转换为特定body类型
* @param
*
* @return
* -<em>相应类型body</em>
*/
toSolid(): BRepSolidBody;
toShell(): BRepShellBody;
toWire(): BRepWireBody;
toAcorn(): BRepAcornBody;
toCompound(): BRepCompoundBody;
findVertex(name: string | string[]): BRepVertex | undefined;
findEdge(name: string | string[]): BRepEdge | undefined;
findFace(name: string | string[]): BRepFace | undefined;
findWire(name: string | string[]): BRepWire | undefined;
findShell(name: string | string[]): BRepShell | undefined;
findSolid(name: string | string[]): BRepSolid | undefined;
findBRepObject(name: string | string[]): BRepObject | undefined;
abstract bndBox(): SupBox;
protected static calcuBox(obj: BRepObject): SupBox;
}
/**
* @brief 实体类型body
*
*/
export declare class BRepSolidBody extends BRepBody {
readonly solid: BRepSolid;
constructor(solid: BRepSolid);
bodyType(): BodyType;
toSolid(): BRepSolidBody;
findVertex(name: string | string[]): BRepVertex | undefined;
findEdge(name: string | string[]): BRepEdge | undefined;
findFace(name: string | string[]): BRepFace | undefined;
findSolid(name: string | string[]): BRepSolid | undefined;
bndBox(): SupBox;
}
/**
* @brief 壳类型body
*
*/
export declare class BRepShellBody extends BRepBody {
readonly shell: BRepShell;
constructor(shell: BRepShell);
bodyType(): BodyType;
toShell(): BRepShellBody;
findVertex(name: string | string[]): BRepVertex | undefined;
findEdge(name: string | string[]): BRepEdge | undefined;
findFace(name: string | string[]): BRepFace | undefined;
findShell(name: string | string[]): BRepShell | undefined;
bndBox(): SupBox;
}
/**
* @brief wire 类型body
*
*/
export declare class BRepWireBody extends BRepBody {
readonly wire: BRepWire;
constructor(wire: BRepWire);
toWire(): BRepWireBody;
bodyType(): BodyType;
findVertex(name: string | string[]): BRepVertex | undefined;
findEdge(name: string | string[]): BRepEdge | undefined;
findWire(name: string | string[]): BRepWire | undefined;
bndBox(): SupBox;
}
/**
* @brief acorn类型body
*
*/
export declare class BRepAcornBody extends BRepBody {
readonly point: BRepVertex;
constructor(point: BRepVertex);
bodyType(): BodyType;
toAcorn(): BRepAcornBody;
findVertex(name: string | string[]): BRepVertex | undefined;
bndBox(): SupBox;
}
/**
* @brief compound类型body
*
*/
export declare class BRepCompoundBody extends BRepBody {
readonly solids: BRepSolid[];
readonly shells: BRepShell[];
readonly wires: BRepWire[];
readonly acorns: BRepVertex[];
bodyType(): BodyType;
toCompound(): BRepCompoundBody;
addSubShape(obj: IBRepObj): boolean;
findVertex(name: string | string[]): BRepVertex | undefined;
findEdge(name: string | string[]): BRepEdge | undefined;
findFace(name: string | string[]): BRepFace | undefined;
findSolid(name: string | string[]): BRepSolid | undefined;
findShell(name: string | string[]): BRepShell | undefined;
findWire(name: string | string[]): BRepWire | undefined;
bndBox(): SupBox;
}
export interface IBRepObj {
type: ShapeType;
props?: IBRepProps;
}
export interface IBRepCompObj extends IBRepObj {
subShapes: IBRepObj[];
}
export declare const createBRepBody: (obj: IBRepObj) => BRepBody | null;

View File

@@ -0,0 +1,20 @@
import { CurveType } from '../geom/CurveType';
import { Polygon } from '../geom/Polygon';
import { BRepObject } from './BRepObject';
import { ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
/**
* @brief BRep边对象
*
*/
export declare class BRepEdge extends BRepObject {
readonly geomType: CurveType;
readonly polygon: Polygon;
readonly start: BRepVertex;
readonly end: BRepVertex;
constructor(obj: any, parent: BRepObject);
isGeom(): boolean;
getAllEdges(): BRepEdge[];
getAllVertices(): BRepVertex[];
shapeType(): ShapeType;
}

View File

@@ -0,0 +1,23 @@
import { SurfaceType } from '../geom/SurfaceType';
import { Triangulation } from '../geom/Triangulation';
import { BRepEdge } from './BRepEdge';
import { BRepLoop } from './BRepLoop';
import { BRepObject } from './BRepObject';
import { ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
/**
* @brief 边界面类型
*
*/
export declare class BRepFace extends BRepObject {
readonly geomType: SurfaceType;
readonly triangulation: Triangulation;
readonly loops: BRepLoop[];
constructor(obj: any, parent: BRepObject);
hasInnerLoop(): boolean;
isGeom(): boolean;
getAllFaces(): BRepFace[];
getAllEdges(): BRepEdge[];
getAllVertices(): BRepVertex[];
shapeType(): ShapeType;
}

View File

@@ -0,0 +1,17 @@
import { BRepEdge } from './BRepEdge';
import { BRepObject } from './BRepObject';
import { ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
/**
* @brief 半边对象
*
*/
export declare class BRepHalfEdge extends BRepObject {
readonly edge: BRepEdge;
readonly reversed: boolean;
constructor(parent: BRepObject, obj: any);
shapeType(): ShapeType;
isGeom(): boolean;
getAllEdges(): BRepEdge[];
getAllVertices(): BRepVertex[];
}

View File

@@ -0,0 +1,18 @@
import { BRepEdge } from './BRepEdge';
import { BRepHalfEdge } from './BRepHalfEdge';
import { BRepObject } from './BRepObject';
import { ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
/**
* @brief BRep环对象用于限定Face的边界
*
*/
export declare class BRepLoop extends BRepObject {
readonly halfEdges: BRepHalfEdge[];
readonly isOut: boolean;
constructor(parent: BRepObject | null, obj: any);
shapeType(): ShapeType;
isGeom(): boolean;
getAllEdges(): BRepEdge[];
getAllVertices(): BRepVertex[];
}

View File

@@ -0,0 +1,29 @@
import { BRepBody } from './BRepBody';
import { BRepEdge } from './BRepEdge';
import { BRepFace } from './BRepFace';
import { ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
export interface IBRepProps {
name: string | string[];
color?: number;
}
/**
* @brief BRep对象接口
*
*/
export declare abstract class BRepObject {
parent: BRepObject | null;
props?: IBRepProps;
constructor(parent: BRepObject | null, obj: any);
name(): string | string[] | undefined;
abstract shapeType(): ShapeType;
abstract isGeom(): boolean;
findEdge(_name: string | string[]): BRepEdge | undefined;
findFace(_name: string | string[]): BRepFace | undefined;
findVertex(_name: string | string[]): BRepVertex | undefined;
getAllFaces(): BRepFace[];
getAllEdges(): BRepEdge[];
getAllVertices(): BRepVertex[];
static findBRep(name: string | string[], array: BRepObject[]): BRepObject | undefined;
body(): BRepBody;
}

View File

@@ -0,0 +1,27 @@
import { BRepBody } from './BRepBody';
import { BRepEdge } from './BRepEdge';
import { BRepFace } from './BRepFace';
import { BRepObject } from './BRepObject';
import { ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
/**
* @brief BRep壳对象壳对象包含面、边和顶点
*
*/
export declare class BRepShell extends BRepObject {
readonly faces: BRepFace[];
readonly edges: BRepEdge[];
readonly vertices: BRepVertex[];
private wrappedBody;
constructor(obj: any, parent?: BRepObject | null);
findVertex(name: string | string[]): BRepVertex | undefined;
findEdge(name: string | string[]): BRepEdge | undefined;
findFace(name: string | string[]): BRepFace | undefined;
shapeType(): ShapeType;
isGeom(): boolean;
getAllFaces(): BRepFace[];
getAllEdges(): BRepEdge[];
getAllVertices(): BRepVertex[];
body(): BRepBody;
setBody(body: BRepBody): void;
}

View File

@@ -0,0 +1,26 @@
import { BRepBody } from './BRepBody';
import { BRepEdge } from './BRepEdge';
import { BRepFace } from './BRepFace';
import { BRepObject } from './BRepObject';
import { BRepShell } from './BRepShell';
import { ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
/**
* @brief BRep实体对象
*
*/
export declare class BRepSolid extends BRepObject {
readonly shells: BRepShell[];
private wrappedBody;
constructor(obj: any);
shapeType(): ShapeType;
isGeom(): boolean;
findVertex(name: string | string[]): BRepVertex | undefined;
findEdge(name: string | string[]): BRepEdge | undefined;
findFace(name: string | string[]): BRepFace | undefined;
getAllFaces(): BRepFace[];
getAllEdges(): BRepEdge[];
getAllVertices(): BRepVertex[];
body(): BRepBody;
setBody(body: BRepBody): void;
}

View File

@@ -0,0 +1,27 @@
/**
* @brief BRep对象类型
* @note 这里定义的BRep对象主要用于显示这里采用Parasolid/ACIS的拓扑表示方式
*
*/
export declare enum ShapeType {
COMPOUND = 0,//! 复合类型
SOLID = 2,//! 实体类型
SHELL = 3,//! 壳类型
FACE = 4,//! 面类型
WIRE = 5,//! 线框类型
EDGE = 6,//! 边类型
VERTEX = 7,//! 顶点类型
LOOP = 9,//! 环类型与a/p对应
HALFEDGE = 10
}
/**
* @brief BRep body类型使用brep对象时应该以此为单位进行使用
*
*/
export declare enum BodyType {
BT_SolidBody = 0,//! 实体对象
BT_ShellBody = 1,//! 壳对象
BT_WireBody = 2,//! 线框对象
BT_AcornBody = 3,//! 点对象
BT_CompoundBody = 4
}

View File

@@ -0,0 +1,18 @@
import { Point } from '../geom/Point';
import { BRepBody } from './BRepBody';
import { BRepObject } from './BRepObject';
import { ShapeType } from './BRepType';
/**
* @brief BRep顶点对象
*
*/
export declare class BRepVertex extends BRepObject {
readonly point: Point;
private wrappedBody;
constructor(obj: any, parent?: BRepObject | null);
isGeom(): boolean;
shapeType(): ShapeType;
getAllVertices(): BRepVertex[];
body(): BRepBody;
setBody(body: BRepBody): void;
}

View File

@@ -0,0 +1,24 @@
import { BRepBody } from './BRepBody';
import { BRepEdge } from './BRepEdge';
import { BRepObject } from './BRepObject';
import { ShapeType } from './BRepType';
import { BRepVertex } from './BRepVertex';
/**
* @brief BRep环对象
*
*/
export declare class BRepWire extends BRepObject {
readonly edges: BRepEdge[];
readonly vertices: BRepVertex[];
private wrappedBody;
constructor();
constructor(obj: any);
findVertex(name: string | string[]): BRepVertex | undefined;
findEdge(name: string | string[]): BRepEdge | undefined;
isGeom(): boolean;
shapeType(): ShapeType;
getAllEdges(): BRepEdge[];
getAllVertices(): BRepVertex[];
body(): BRepBody;
setBody(body: BRepBody): void;
}

View File

@@ -0,0 +1,12 @@
export * from './BRepBody';
export * from './BRepEdge';
export * from './BRepFace';
export * from './BRepHalfEdge';
export * from './BRepLoop';
export * from './BRepObject';
export * from './BRepShell';
export * from './BRepSolid';
export * from './BRepType';
export * from './BRepVertex';
export * from './BRepWire';
export * from './BRepAssembly';

View File

@@ -0,0 +1,43 @@
import { Vector3, Vector2 } from 'three';
export declare class AABB {
readonly lowPoint: Vector3 | Vector2;
readonly highPoint: Vector3 | Vector2;
/**
* @remarks
* If the min is higher than the max, the bounding box calculations will break
*
* @param minX the minimum value in the X axis
* @param minY the minimum value in the Y axis
* @param minZ the minimum value in the Z axis
* @param maxX the maximum value in the X axis
* @param maxY the maximum value in the Y axis
* @param maxZ the maximum value in the Z axis
*/
constructor(minX: number, minY: number, minZ: number | null, maxX: number, maxY: number, maxZ: number | null);
/**
* Merges the two bounding boxes to create a larger one containing the two
*
* @param other - The other bounding box to use for merging
* @returns The new bounding box containing both bounding boxes
*/
merge(other: AABB): AABB;
/**
* Check if the other bounding box is overlapping with this one
*
* @param other - The bounding box to check overlap against
* @returns `true` if the other bounding box is colling, `false` otherwise
*/
overlaps(other: AABB): boolean;
/**
* Check if the point is contained within the bounding box
*
* @param point - The point to check
* @returns `true` if the point is inside the bounding box, `false` otherwise
*/
containsPoint(point: Vector3 | Vector2): boolean;
get is3D(): boolean;
get width(): number;
get height(): number;
get depth(): number;
get space(): number;
}

View File

@@ -0,0 +1,38 @@
import { IAABBShape } from './IAABBShape';
import { AABB } from './AABB';
/**
* Axis Aligned Bouding Box Node
*
* @remark
* The bounding box on this nodes either fully contains the shape or the two child nodes.
*/
interface IAABBNode {
Aabb: AABB;
Shape?: IAABBShape;
ParentNode?: IAABBNode;
LeftNode?: IAABBNode;
RightNode?: IAABBNode;
readonly IsLeaf: boolean;
}
export declare class AABBNode implements IAABBNode {
Aabb: AABB;
Shape?: IAABBShape;
ParentNode?: IAABBNode;
LeftNode?: IAABBNode;
RightNode?: IAABBNode;
/**
* @param aabb - A bounding box containing the shape or the two child nodes
* @param shape - A reference to a shape implementation of IAABBShape
* @param parentNode - The node parenting this node
* @param leftNode - The first child node
* @param rightNode - The second child node
*/
constructor(Aabb: AABB, shape?: IAABBShape, parentNode?: IAABBNode, leftNode?: IAABBNode, rightNode?: IAABBNode);
/**
* @remark
* The node is considered a leaf when is has no reference to any child nodes.
* Only leaf nodes should contain shapes
*/
get IsLeaf(): boolean;
}
export {};

View File

@@ -0,0 +1,61 @@
import { Ray, Vector3, Vector2 } from 'three';
import { IAABBShape } from './IAABBShape';
import { AABBNode } from './AABBNode';
/**
* Axis Aligned Bounding Box Tree
*/
export declare class AABBTree {
private rootNode?;
private shapeToNodeMap;
constructor();
/**
* Adds a new shape to the tree.
* @param shape The shape to add to the tree (Must be an implementation of IAABBShape interface)
*/
addShape(shape: IAABBShape): void;
/**
* Removes the shape and balances the tree
* @remarks The method will gracefully exit if the shape doesn't exist in the tree
* @param shape - The shape to remove from the tree
*/
removeShape(shape: IAABBShape): void;
/**
* Finds all the shape overlapping with the point
* @remarks if the tree is empty this function will always return an empty array
* @param shape - The shape to check overlaps against
* @returns An array containing all the shape overlapping with the point
*/
getShapesOverlappingWith(point: Vector3 | Vector2): IAABBShape[];
/**
* Returns all the node in the tree
*
* @remarks
* The nodes that serves as parent containers are also returned. Do not expect every node to contain a shape
*
* @returns An array containing all the nodes the tree contains
*/
getAllNodes(): AABBNode[];
/**
* Iterates recursivly over the entire three and all the node to the array
*
* @param node - The node to iterate over
* @param array - Reference to the array that will contain all the nodes in the tree
*/
private nodeIterator;
/**
* Remove a node from the tree and balances the tree if needed
*
* @param node - The node to remove from the tree
*/
private removeNode;
/**
* @remarks
* When the parent node intersects the ray, the child node is found separately.
* If a Boundbox for a leaf node intersects a ray, the leaf node is returned
*
* @notion two dimensional ray intersection is not achieved
* @param ray A ray type from threejs
* @returns All shapes that intersect the Ray
*/
ray2Tree(ray: Ray): IAABBShape[] | null;
}

View File

@@ -0,0 +1,7 @@
import { Vector3, Vector2 } from 'three';
import { AABB } from './AABB';
export interface IAABBShape {
getAABB(): AABB;
containsPoint(point: Vector3 | Vector2): boolean;
getTypeName(): string;
}

View File

@@ -0,0 +1,13 @@
import { Vector3, Vector2 } from 'three';
import { IAABBShape } from './IAABBShape';
import { AABB } from './AABB';
export declare class LineAABBShape implements IAABBShape {
private aabb;
private VectorList;
private type;
constructor(A: Vector3 | Vector2, B: Vector3 | Vector2);
getAABB(): AABB;
containsPoint(point: Vector3 | Vector2): boolean;
getTypeName(): string;
points(): (Vector3 | Vector2)[];
}

View File

@@ -0,0 +1,13 @@
import { Vector3, Vector2 } from 'three';
import { IAABBShape } from './IAABBShape';
import { AABB } from './AABB';
export declare class TriAABBShape implements IAABBShape {
private aabb;
private VectorList;
private type;
constructor(A: Vector3 | Vector2, B: Vector3 | Vector2, C: Vector3 | Vector2);
getAABB(): AABB;
containsPoint(point: Vector3 | Vector2): boolean;
getTypeName(): string;
points(): (Vector3 | Vector2)[];
}

View File

@@ -0,0 +1,6 @@
export * from './AABB';
export * from './AABBNode';
export * from './AABBTree';
export * from './IAABBShape';
export * from './LineAABBShape';
export * from './TriAABBShape';

View File

@@ -0,0 +1,17 @@
import { SupVector } from '../types';
/**
* @brief 坐标对象
* @note 坐标对象目前使用threejs的Vector3实现
*
*/
export declare class Coord {
readonly xyz: SupVector;
constructor(x: number, y: number, z: number);
setCoord(x: number, y: number, z: number): void;
x(): number;
y(): number;
z(): number;
static toFloat32Array(coords: Coord[]): Float32Array;
static toArray(coords: Coord[]): number[];
static toArrayVec(coords: SupVector[]): number[];
}

View File

@@ -0,0 +1,10 @@
import { SupMat4, SupVector } from '../types';
export declare class Coordinate {
readonly pos: SupVector;
readonly zDir: SupVector;
readonly xDir: SupVector;
constructor(pos?: SupVector, zDir?: SupVector, xDir?: SupVector);
get yDir(): SupVector;
private normalize;
toMatrix4(): SupMat4;
}

View File

@@ -0,0 +1,15 @@
/**
* @brief 曲线类型枚举
*
*/
export declare enum CurveType {
Line = 0,//! 直线
Circle = 1,//! 圆
Ellipse = 2,//! 椭圆
Parabola = 3,//! 抛物线
Hyperbola = 4,//! 双曲线
OffsetCurve = 5,//! 偏置曲线
BezierCurve = 6,//! 贝塞尔曲线
BSplineCurve = 7,//! Nurbs曲线
Other = 8
}

View File

@@ -0,0 +1,10 @@
import { Coord } from './Coord';
import { SupMat4 } from '../types';
/**
* @brief 方向对象
*
*/
export declare class Direct extends Coord {
constructor(x: number, y: number, z: number);
applayMatrix4(mat: SupMat4): void;
}

View File

@@ -0,0 +1,174 @@
import { Coordinate } from './Coordinate';
import { PolyGeometry } from './PolyGeometry';
import { Triangulation } from './Triangulation';
import { SupBox, SupVector } from '../types';
type NumberArray4 = [number, number, number, number];
export type CRModeType = 'Chamfer' | 'Fillet';
export type CRModeTypeArray4 = [CRModeType, CRModeType, CRModeType, CRModeType];
export declare class GeometryBuilder {
static angleTol: number;
static zeroTol: number;
private static floatEqual;
static cone(r1: number, r2: number, l: number, csys?: Coordinate): Triangulation | null;
static arrow(cylR: number, cylL: number, coneR: number, coneL: number, csys?: Coordinate): Triangulation | null;
static cutPlaneByBox(box: SupBox, csys: Coordinate): PolyGeometry | null;
static box(min: SupVector, max: SupVector, csys?: Coordinate): PolyGeometry | null;
static sphere(radius: number, center: SupVector, csys?: Coordinate, segments?: number): Triangulation;
/**
* 计算一段圆弧上的离散的点
* @param center 圆心
* @param radius 半径
* @param startAngle 开始角度
* @param endAngle 结束角度
* @param segments 线段数量
* @param returnMode 1, 2, 3, 4 返回模式
* @param xy true 在 xy 平面上 false 在 xz平面上
* @returns
* returnMode | length | means
* ------------------------------------------------
* 1 | segments | start + paths
* 2 | segments | paths + end
* 3 | segments + 1 | start + paths + end
* 4 | segments - 1 | paths
*/
private static computeArcNodes;
/**
* geneate line with connecting start index to end index
* for example:
* input: 0, 3
* output: [0,1,1,2,2,3]
* @param startIndex start index
* @param endIndex end index
* @returns line array connect start index to end index
*/
private static generateArcLine;
/**
* 使用多个圆环创建面和圆环线
* loops length | first loop length | result surface
* --------------------------------------------------
* 1 | 1 | point -> error
* 1 | >= 3 | plane -> circle plane
* 2 | 1 | circle plane or cone surface
* 2 | === another | torus plane or cone plane
* >2 | === others | torus surface
* @param loops 圆环数组
* @param poly 添加到的目标PolyGeometry
* @param lineMode
* 0: don't add lines
* 1: add first loop lines
* 2: add last loop lines
* 3. add first and last loop lines
* @param close 是否形成闭环,头尾相接
* @param z true: the surface normal direction is z positive; false: the surface normal direction is z negative
*/
private static addSurfaceByLoops;
/**
* 球或空心球的2D草图
* @param r1 大圆半径
* @param r2 小圆半径 为0时球大于0时空心球 大于等于r1时报错
* @param center 圆心
* @param quarter false: full ; true: 1/4 circle
* @param segments 一个圆的分段数
* @param csys 坐标系
*/
static sphere2D(r1: number, r2: number, center?: SupVector, quarter?: boolean, segments?: number, csys?: Coordinate | undefined): PolyGeometry;
/**
* 构造圆环的草图
* @param radius 截面半径
* @param center 截面圆心
* @param quarter false: full quarter: 半圆
* @param segments 整圆分段数
* @param csys 坐标系
* @returns
*/
static torus2D(radius: number, center: SupVector, quarter?: boolean, segments?: number, csys?: Coordinate | undefined): PolyGeometry;
/**
* 根据点 和 anchors 创建 2D 轮廓图形
* @param poly 目标PolyGeometry
* @param nodes 一些点坐标
* @param anchors 锚点,创建线的起止点
*/
private static create2DShape;
/**
* R1
* |<---> __ ←-
* | r2 / \ r1 p1↑ 点创建顺序
* | | |
* | | |
* | | |
* | r3 \__/ r4
* |<--------->
* pos R2
*
* @param R1 小圆半径
* @param R2 大圆半径
* @param height 高度
* @param position 旋转轴位置
* @param radii 倒角半径 4 个
* @param modes 倒角类型 4个
* @param quarter 是否 1/4
* @param segments 倒圆角的时候圆弧的分辨率
* @returns nodes 和 anchors 和 middle( 上 与 下 的 分界点)
*/
private static cylinder2DNodes;
static cylinder(R1: number, R2: number, height: number, position?: SupVector, radii?: NumberArray4, modes?: CRModeTypeArray4, segments?: number, revolution?: boolean, range?: [number, number], quarter?: boolean, csys?: Coordinate | undefined): PolyGeometry;
/**
* |
* | _________ ——
* | r2 / \ r1 p0 ↑
* | | ·p | y
* | r3 \_________/ r4 ↓
* |pos |<--------->| ——
* | x
* |————————————————————————
* @param x box x direction length
* @param y box y direction length
* @param position box center position
* @param radii r1, r2, r3, r4
* @param modes m1, m2, m3, m4
* @param segments one arc segments( Π / 2)
*/
private static box2DNodes;
/**
* 创建的基本体的顶面或者底面的平面
* @param poly 目标PolyGeometry
* @param nodes 一圈平面点
* @param anchors 锚点,创建线的起止点
* @param z true: 法向z正方形 false: 法向为z的负方向
*/
private static addOneTopOrBottomPlane;
/**
* 添加一个侧面可能是一个平面或者一个圆柱面平面由4个点2个三角形组成圆柱面由2n个点2n个三角形组成
* @param poly 目标PolyGeometry
* @param nodes1 底面的点
* @param nodes2 顶面的点
*/
private static addOneSideSurface;
/**
* 复杂的box, 可对4个角进行倒角
* @param x x方向长度
* @param y y方向长度
* @param z z方向长度
* @param position 底面圆心位置
* @param radii 4个角的倒角半径
* @param modes 4个角的倒角模式
* @param segments Math.PI / 4 的分段数
* @param extrude 是否extrude , false返回2D图形
* @param csys 坐标系
* @returns PolyGeometry
*/
static complexBox(x: number, y: number, z: number, position?: SupVector, radii?: NumberArray4, modes?: CRModeTypeArray4, segments?: number, extrude?: boolean, csys?: Coordinate | undefined): PolyGeometry;
/**
* 构造正多边形棱柱
* @param radius 外接圆半径
* @param height 高度
* @param edgeNumber 边数量
* @param startAngle 起始点位置
* @param bottomCenter 底面圆心位置
* @param extrude true: 拉伸的3D模型 false: 未拉伸的2D轮廓
* @param csys 坐标系
* @returns poly geometry
*/
static regularPolygonalPrism(radius: number, height: number, edgeNumber: number, startAngle?: number, bottomCenter?: SupVector, extrude?: boolean, csys?: Coordinate | undefined): PolyGeometry;
}
export {};

View File

@@ -0,0 +1,9 @@
import { Coord } from './Coord';
import { SupMat4 } from '../types';
/**
* @brief 点对象
*
*/
export declare class Point extends Coord {
applayMatrix4(mat: SupMat4): void;
}

View File

@@ -0,0 +1,49 @@
import { Ray } from 'three';
import { Triangle } from './Triangulation';
import { SupBox, SupVector } from '../types';
import { AABBTree } from './AABB/AABBTree';
export type PolyLine = {
indices: number[];
isSegment: boolean;
props?: object;
};
export type PolySurf = {
triangles: Triangle[];
props?: object;
};
export interface IPolyGeometry {
nodes: Array<[number, number, number]>;
surfs?: Array<{
props?: object;
triangles: Array<[number, number, number]>;
}>;
lines?: Array<{
props?: object;
indices: Array<[number, number]> | number[];
}>;
}
/**
* @brief poly几何可以包含多个多边形面和多边形边
*
*/
export declare class PolyGeometry {
readonly nodes: SupVector[];
readonly lines: PolyLine[];
readonly surfs: PolySurf[];
constructor(nodes?: SupVector[]);
static createIndex(poly: PolyLine): number[];
bndBox(): SupBox;
static createFromIPoly(data: IPolyGeometry): PolyGeometry;
static ray2Tree(ray: Ray, tree: AABBTree): number;
/**
* AABB Tree is used to calculate the distance between two PolyGeometrys
* @param geomFrom PolyGeometry
* @param geomTo PolyGeometry
* @param direction The direction from geomFrom to geomTo
* @returns The distance from geomFrom to geomTo calculated along the direction.
* If the return value is negative, it represents the distance from geomFrom to geomTo in the opposite direction.
* If the return value is positive, it represents the distance from geomFrom to geomTo in the positive direction.
* If the return value is null, it means that geomFrom and geomTo cannot calculate the distance in this direction.
*/
static calDistancewithAABB(geomFrom: PolyGeometry, geomTo: PolyGeometry, direction: SupVector): number | null;
}

View File

@@ -0,0 +1,14 @@
import { Point } from './Point';
import { SupBox } from '../types';
/**
* @brief 多边形对象
*
*/
export declare class Polygon {
protected nodes: Point[];
constructor();
get nodeList(): Point[];
set nodeList(nodes: Point[]);
static createIndex(size: number, offset: number): number[];
bndBox(): SupBox;
}

View File

@@ -0,0 +1,17 @@
/**
* @brief 曲面类型
*
*/
export declare enum SurfaceType {
Plane = 0,//! 平面
Cylinder = 1,//! 圆柱面
Cone = 2,//! 圆锥面
Sphere = 3,//! 球面
Torus = 4,//! 圆环面
BezierSurface = 5,//! 贝塞尔曲面
BSplineSurface = 6,//! Nurbs曲面
SurfaceOfRevolution = 7,//! 回转面
SurfaceOfExtrusion = 8,//! 拉伸面
OffsetSurface = 9,//! 偏执面
OtherSurface = 10
}

Some files were not shown because too many files have changed in this diff Show More