完整备份: 纳入全部源文件/数据/文档(含 PDF/STEP/Excel/编译产物)

This commit is contained in:
wangruiguo
2026-09-03 18:04:37 +08:00
parent c286aed1d9
commit dd215b6679
4445 changed files with 1646144 additions and 39 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
ONEBOT Pneumatic Catalog 2026.08
Double-click OnebotCatalog.exe to run (no installation, Win10/11).
Autoplay is disabled by default on Win10/11 - run the exe manually.
Update: replace catalog.opc with the new version.
Note: unsigned software may trigger antivirus warnings - add to allow list if needed.

View File

@@ -0,0 +1,8 @@
欧霓博气动元件电子目录 2026.08
双击 OnebotCatalog.exe 运行 (Win10/11 免安装)。
Win10/11 默认禁用自动播放, 请手动双击运行。
目录更新: 用新版本 catalog.opc 替换本目录中的同名文件即可。
常见问题: 若杀毒软件拦截, 请将程序加入白名单 (本软件无签名)。

Binary file not shown.

View File

@@ -0,0 +1,3 @@
[autorun]
open=OnebotCatalog.exe
icon=OnebotCatalog.exe,0

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
# 禁止直接访问加密模型文件双保险PHP 接口已校验令牌)
<FilesMatch "\.(stp|step|enc)$">
Require all denied
</FilesMatch>

View File

@@ -0,0 +1,66 @@
<?php
/**
* STEP 查看器 - 模型读取接口PHP 版)
* 校验令牌 → .enc 解密 / .stp 原样 → 流式返回
*/
$SECRET = '7175a8598ff7fa1c182f57f0c750e4f8';
// 与 upload.php 保持一致的私有目录配置:模型放 Web 目录外web 里根本没有文件可下载
$PRIVATE_DIR = '/volume1/stepviewer_private';
function json_out($code, $obj) {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
$f = $_GET['f'] ?? '';
$t = $_GET['t'] ?? '';
$e = (int)($_GET['e'] ?? 0);
if (!$f || !$t || !preg_match('/\.(stp|step|enc)$/i', $f)) json_out(400, ['error' => 'bad params']);
$sig = substr(hash_hmac('sha256', $f . '|' . $e, $SECRET), 0, 16);
if (!hash_equals($sig, $t) || time() > $e) json_out(403, ['error' => 'invalid or expired token']);
$root = realpath(__DIR__ . '/..');
if ($PRIVATE_DIR !== '') {
// 私有目录优先(嵌入链接写纯文件名即可);目录不存在时退回站内路径,
// 让留在 web 目录里的样例模型按原路径继续可用(这些文件本来就允许直链,
// 退回不新增暴露面)。要保护的文件必须移出 web 目录放进私有目录。
$full = realpath($PRIVATE_DIR . '/' . basename($f));
$ok = $full && strpos($full, realpath($PRIVATE_DIR)) === 0 && is_file($full);
if (!$ok) {
$full = realpath($root . '/' . $f);
$ok = $full && strpos($full, $root) === 0 && is_file($full);
}
} else {
$full = realpath($root . '/' . $f);
$ok = $full && strpos($full, $root) === 0 && is_file($full);
}
if (!$ok || !$full) json_out(404, ['error' => 'not found']);
$data = file_get_contents($full);
if (preg_match('/\.enc$/i', $f)) {
$nonce = substr($data, 0, 16);
$key = hash('sha256', $SECRET . $nonce, true);
$body = substr($data, 16);
$out = '';
for ($i = 0; $i < strlen($body); $i++) {
$out .= $body[$i] ^ $key[$i % strlen($key)];
}
$data = $out;
}
// 传输层 gzip新格式文件解密后即以 gzip 魔数开头,原样传输;
// 旧格式明文文件在线压缩。浏览器 fetch 自动解压(无需前端改动)
$send = $data;
if (substr($send, 0, 2) === "\x1f\x8b") {
header('Content-Encoding: gzip');
} elseif (!ini_get('zlib.output_compression') && strlen($data) > 1024) {
$gz = gzencode($data, 6);
if ($gz !== false) { $send = $gz; header('Content-Encoding: gzip'); }
}
header('Content-Type: application/octet-stream');
header('Content-Length: ' . strlen($send));
header('Cache-Control: no-store');
echo $send;

View File

@@ -0,0 +1,35 @@
<?php
/**
* STEP 查看器 - 令牌接口PHP 版)
* 同源校验 + HMAC 短时令牌5 分钟)
*/
header('Content-Type: application/json; charset=utf-8');
$SECRET = '7175a8598ff7fa1c182f57f0c750e4f8';
$TTL = 300;
function json_out($code, $obj) {
http_response_code($code);
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
$f = $_GET['f'] ?? '';
if (!$f || !preg_match('/\.(stp|step|enc)$/i', $f)) json_out(400, ['error' => 'bad file']);
// 只允许本站页面来取令牌,挡掉直接爬取。
// 优先看 Sec-Fetch-Site由查看器页面发起的同源 fetch 恒为 same-origin跨站 iframe
// 嵌入也不受影响,且不会被父站点的 Referrer-Policy 剥掉);没有该头的旧浏览器
// 退回「Referer 含本站 host」的校验。直链/curl 两种都过不了。
$fetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '';
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
if ($fetchSite !== '') {
if ($fetchSite !== 'same-origin' && $fetchSite !== 'same-site') json_out(403, ['error' => 'forbidden']);
} elseif (strpos($referer, $host) === false) {
json_out(403, ['error' => 'forbidden']);
}
$exp = time() + $TTL;
$sig = substr(hash_hmac('sha256', $f . '|' . $exp, $SECRET), 0, 16);
json_out(200, ['token' => $sig, 'expires' => $exp]);

View File

@@ -0,0 +1,98 @@
<?php
/**
* STEP 查看器 - 上传接口PHP 版,虚拟主机可用)
* 接收原始字节流 + X-Filename 头,加密存储为 uploads/xxx.enc
* 生产环境请修改 SECRET
*/
header('Content-Type: application/json; charset=utf-8');
$SECRET = '7175a8598ff7fa1c182f57f0c750e4f8';
$MAX_SIZE = 500 * 1024 * 1024;
// 私有目录Web 目录外的绝对路径模型只存这里web 目录里根本没有模型文件可下载。
// 群晖 File Station 在 web 目录外建 /volume1/stepviewer_private 并给 http 用户组读写权限;
// 路径不同就改成你的实际路径。
$PRIVATE_DIR = '/volume1/stepviewer_private';
function json_out($code, $obj) {
http_response_code($code);
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') json_out(405, ['error' => 'method not allowed']);
$len = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
if ($len <= 0) json_out(400, ['error' => 'empty body']);
if ($len > $MAX_SIZE) json_out(413, ['error' => 'file too large (max 500MB)']);
$raw = urldecode($_SERVER['HTTP_X_FILENAME'] ?? 'model.step');
$base = basename($raw);
$safe = preg_replace('/[^\w.\-\x{4e00}-\x{9fff}]/u', '_', $base);
if (!$safe || !preg_match('/\.(step|stp)$/i', $safe)) $safe .= '.stp';
$stem = pathinfo($safe, PATHINFO_FILENAME);
$ext = pathinfo($safe, PATHINFO_EXTENSION);
$name = $stem . '_' . substr(bin2hex(random_bytes(4)), 0, 8) . '.' . $ext;
$data = file_get_contents('php://input');
if ($data === false || strlen($data) !== $len) json_out(400, ['error' => 'incomplete upload']);
// 客户端可选 gzip 压缩上传STEP 文本约可压到 1/6节省带宽
if (($_SERVER['HTTP_X_ENCODING'] ?? '') === 'gzip') {
$dec = gzdecode($data);
if ($dec === false) json_out(400, ['error' => 'bad gzip body']);
if (strlen($dec) > $MAX_SIZE) json_out(413, ['error' => 'file too large (max 500MB)']);
$data = $dec;
}
// 存储层也压缩:先 gzip 再加密,磁盘占用约 1/6读取端靠 gzip 魔数自动识别)
$rawSize = strlen($data);
$data = gzencode($data, 6);
if ($data === false) json_out(500, ['error' => 'gzip failed']);
$dir = $PRIVATE_DIR !== '' ? $PRIVATE_DIR : (__DIR__ . '/../uploads');
if (!is_dir($dir)) @mkdir($dir, 0755, true);
if (!is_dir($dir)) json_out(500, ['error' => 'upload dir not writable']);
$nonce = random_bytes(16);
$key = hash('sha256', $SECRET . $nonce, true);
$enc = '';
for ($i = 0; $i < strlen($data); $i++) {
$enc .= $data[$i] ^ $key[$i % strlen($key)];
}
file_put_contents($dir . '/' . $name . '.enc', $nonce . $enc);
// 私有目录模式下返回裸文件名:嵌入链接就是 ?file=xxx.enc读取端按 basename 在私有目录找
$entryPath = ($PRIVATE_DIR !== '' ? '' : 'uploads/') . $name . '.enc';
// 记入首页数模库(入口目录的 library.json2D/3D 共享);库写失败不影响上传本身
try {
add_to_library($entryPath, $safe, '3d');
} catch (Throwable $e) { /* 忽略 */ }
json_out(200, ['path' => $entryPath, 'size' => $rawSize]);
/**
* 把刚上传的文件追加进首页数模库(../../library.json
* 同路径已有记录就刷新名称和时间,否则插到最前。
*/
function add_to_library($path, $name, $type) {
$libFile = dirname(__DIR__, 2) . '/library.json';
$records = [];
if (is_file($libFile)) {
$old = json_decode((string)file_get_contents($libFile), true);
if (is_array($old) && isset($old['records']) && is_array($old['records'])) $records = $old['records'];
}
$found = false;
foreach ($records as $i => $rec) {
if (isset($rec['path']) && $rec['path'] === $path) {
$records[$i]['name'] = $name;
$records[$i]['time'] = time();
$found = true;
break;
}
}
if (!$found) {
array_unshift($records, ['path' => $path, 'name' => $name, 'type' => $type, 'note' => '', 'time' => time()]);
}
@file_put_contents($libFile, json_encode(['records' => $records], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>STEP 查看器嵌入示例</title>
<style>
body { margin: 0; font-family: "Microsoft YaHei", sans-serif; background: #f5f6f7; }
.header {
background: #fff; padding: 14px 24px; border-bottom: 1px solid #e3e6e8;
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
}
.header h1 { font-size: 18px; margin: 0; color: #2b9ae8; }
.model-btn {
border: 1px solid #ddd; background: #fff; border-radius: 4px;
padding: 8px 16px; cursor: pointer; font-size: 13px; color: #444;
}
.model-btn:hover { border-color: #2b9ae8; color: #2b9ae8; }
.content { padding: 16px; }
.frame-box {
background: #fff; border: 1px solid #e3e6e8; border-radius: 6px;
overflow: hidden; height: calc(100vh - 150px);
}
iframe { width: 100%; height: 100%; border: none; display: block; }
.tip { color: #888; font-size: 12px; margin-top: 8px; line-height: 1.8; }
</style>
</head>
<body>
<div class="header">
<h1>STEP 3D 查看器 · 嵌入示例</h1>
<button class="model-btn" data-model="samples/user/LZ01001001_H100_2mm.stp">LZ01001001_H100</button>
<button class="model-btn" data-model="samples/user/LZ01001005_H500_2mm.stp">LZ01001005_H500</button>
<button class="model-btn" data-model="samples/as1-oc-214.stp">as1 装配体</button>
</div>
<div class="content">
<div class="frame-box">
<!-- 嵌入方式iframe + ?file= 参数自动加载指定模型 -->
<iframe id="viewerFrame" src="./index.html?file=samples/user/LZ01001001_H100_2mm.stp" allowFullScreen></iframe>
</div>
<div class="tip">
嵌入方法:&lt;iframe src="查看器目录/index.html?file=模型路径.stp" width="100%" height="600" allowFullScreen&gt;&lt;/iframe&gt;<br>
部署:把整个 STEPViewer 目录index.html、app.js、style.css、libs、icons上传到你的网站iframe 指向它即可。<br>
注意:模型文件要与查看器同源(同一域名下),或用 ?url= 加载远程模型(远程服务器需允许 CORS
</div>
</div>
<script>
document.querySelectorAll('.model-btn').forEach((b) => {
b.addEventListener('click', () => {
document.getElementById('viewerFrame').src = './index.html?file=' + encodeURIComponent(b.dataset.model);
});
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>STEP 模型查看器</title>
<link rel="icon" href="favicon.ico">
<link rel="stylesheet" href="./style.css?v=5">
<script type="importmap">
{
"imports": {
"three": "./libs/three.module.min.js",
"three/addons/": "./libs/"
}
}
</script>
<script src="./libs/occt-import-js.js"></script>
</head>
<body>
<script>
// 首帧渲染前应用 embed/menu-min 样式, 消除加载瞬间完整界面(顶栏/模型树/完整菜单)闪烁 (欧霓博目录专用补丁 2026-08-24)
(function () {
var q = new URLSearchParams(location.search);
if (q.get('embed') === '1') document.body.classList.add('embed');
if (q.get('menu') === 'min') document.body.classList.add('menu-min');
})();
</script>
<!-- 顶栏 -->
<div id="topbar">
<div class="logo">
<span class="logo-text">STEP 3D 查看器</span>
</div>
<button id="btnOpen" class="btn-primary" title="打开 STEP 文件">打开模型</button>
<label class="quality-label" title="网格细分精度(越高越精细,转换时间越长)">精度
<select id="qualitySelect">
<option value="low"></option>
<option value="medium"></option>
<option value="high" selected></option>
</select>
</label>
<button id="btnEmbed" class="btn-secondary" title="生成嵌入链接">嵌入</button>
<span id="fileName" class="file-name">未加载模型(支持拖拽 .step/.stp 文件到页面)</span>
<div class="top-right">
<button id="btnFullscreen" class="icon-btn" title="全屏"><img src="./icons/all-screen.png" alt="全屏"></button>
</div>
</div>
<div id="main">
<!-- 左侧模型树 -->
<div id="treePanel">
<div class="tree-header">
<span class="tree-title">模型树</span>
<button id="btnTreeClose" class="tree-close" title="关闭面板"></button>
<div class="tree-search">
<img src="./icons/ico-search.33e9e6a1.png" alt="搜索">
<input id="treeSearch" type="text" placeholder="搜索零件...">
</div>
</div>
<div id="tree" class="tree-body"><div class="tree-empty">加载模型后显示零件树</div></div>
</div>
<!-- 视口 -->
<div id="viewport">
<div id="canvasWrap"></div>
<!-- 底部工具栏 -->
<div id="toolbar">
<button class="tool-btn active" data-tool="select" title="选择"><img src="./icons/view-tool-select.png" alt="选择"><span>选择</span></button>
<button class="tool-btn" data-tool="measure" title="测量"><img src="./icons/view-tool-distance.png" alt="测量"><span>测量</span></button>
<button class="tool-btn" data-tool="drag" title="拖动零件"><img src="./icons/view-tool-drag.png" alt="拖动"><span>拖动</span></button>
<button class="tool-btn" data-tool="singalDrag" title="单件拖动"><img src="./icons/view-tool-singalDrag.png" alt="单件拖动"><span>单件</span></button>
<button class="tool-btn" data-tool="explode" title="爆炸"><img src="./icons/view-tool-blast.png" alt="爆炸"><span>爆炸</span></button>
<button class="tool-btn" data-tool="section" title="剖切"><img src="./icons/view-tool-slice.png" alt="剖切"><span>剖切</span></button>
<div class="tool-sep"></div>
<button class="tool-btn" id="btnTree" title="结构树显示/隐藏"><img src="./icons/view-tool-tree.png" alt="结构树"><span>结构树</span></button>
<button class="tool-btn" id="btnWireframe" title="线框(显示/隐藏轮廓线)"><img src="./icons/ico-set-wireframe.png" alt="线框"><span>线框</span></button>
<button class="tool-btn" id="btnColorPanel" title="上色"><img src="./icons/ico-set-color.png" alt="上色"><span>上色</span></button>
<button class="tool-btn" id="btnShowAll" title="全部显示"><img src="./icons/view-tool-hideShow.png" alt="全部显示"><span>显示</span></button>
<button class="tool-btn" id="btnTransparent" title="透明(选中零件)"><img src="./icons/view-tool-transparent.png" alt="透明"><span>透明</span></button>
<button class="tool-btn" id="btnBbox" title="包围盒"><img src="./icons/view-tool-coutBox.png" alt="包围盒"><span>包围盒</span></button>
<button class="tool-btn" id="btnClear" title="清除测量"><img src="./icons/view-tool-clear.png" alt="清除"><span>清除</span></button>
<button class="tool-btn" id="btnSingleReset" title="单件复位"><img src="./icons/view-tool-singalReset.png" alt="单件复位"><span>复位</span></button>
<button class="tool-btn" id="btnAllReset" title="全部复位"><img src="./icons/view-tool-allReset.png" alt="全部复位"><span>全复位</span></button>
<div class="tool-sep"></div>
<button class="tool-btn" id="btnVport" title="切换视角"><img src="./icons/view-tool-vport.png" alt="视角"><span>视角</span></button>
<button class="tool-btn" id="btnAxes" title="坐标系"><img src="./icons/view-tool-coordinate.png" alt="坐标系"><span>坐标</span></button>
<button class="tool-btn" id="btnFull" title="全屏"><img src="./icons/all-screen.png" alt="全屏"><span>全屏</span></button>
</div>
<!-- 爆炸面板 -->
<div id="explodePanel" class="float-panel hidden">
<div class="panel-title">爆炸 <img src="./icons/close.0fb8dc6e.png" class="panel-close" data-panel="explode" alt="关闭"></div>
<div class="panel-row">
<span>程度</span>
<input type="range" id="explodeSlider" min="0" max="100" value="0">
<span id="explodeVal" class="panel-val">0%</span>
</div>
</div>
<!-- 剖切面板 -->
<div id="sectionPanel" class="float-panel hidden">
<div class="panel-title">剖切 <img src="./icons/close.0fb8dc6e.png" class="panel-close" data-panel="section" alt="关闭"></div>
<div class="panel-row">
<button class="axis-btn" data-axis="x">X</button>
<button class="axis-btn" data-axis="y">Y</button>
<button class="axis-btn active" data-axis="z">Z</button>
<button id="btnSectionFlip" class="small-btn">翻转</button>
<button id="btnSectionToggle" class="small-btn on">开启</button>
</div>
<div class="panel-row">
<span>位置</span>
<input type="range" id="sectionSlider" min="-100" max="100" value="0">
<span id="sectionVal" class="panel-val">0</span>
</div>
</div>
<!-- 测量子面板(水平单行,激活时隐藏底部工具栏) -->
<div id="measurePanel" class="float-panel measure-panel hidden">
<div id="measureModes"></div>
<div class="measure-actions">
<button id="btnStats" class="measure-act" title="结果统计"><img src="./icons/view-tool-statisticalResult.png" alt="统计"></button>
<button id="btnMeasureClear" class="measure-act" title="清空"><img src="./icons/view-tool-clear.png" alt="清空"></button>
</div>
<button id="btnMeasureClose" class="measure-act" title="关闭测量,恢复工具栏"><img src="./icons/close.0fb8dc6e.png" alt="关闭"></button>
</div>
<!-- 结果统计面板 -->
<div id="statsPanel" class="float-panel hidden">
<div class="panel-title">结果统计 <img src="./icons/close.0fb8dc6e.png" class="panel-close" data-panel="stats" alt="关闭"></div>
<div id="statsList"><div class="stats-empty">暂无测量结果</div></div>
</div>
<!-- 上色面板 -->
<div id="colorPanel" class="float-panel hidden">
<div class="panel-title">上色 <img src="./icons/close.0fb8dc6e.png" class="panel-close" data-panel="color" alt="关闭"></div>
<div class="panel-row">
<input type="color" id="colorPicker" value="#2b9ae8" title="选择颜色">
<button id="btnColorApply" class="small-btn">应用到选中</button>
</div>
<div class="panel-row">
<button id="btnColorOneClick" class="small-btn" title="按零件自动分配颜色"><img src="./icons/one-click-color.0a5c28c0.png" alt="一键上色">一键上色</button>
<button id="btnColorReset" class="small-btn">恢复原色</button>
</div>
<div class="panel-row">
<span>亮度</span>
<input type="range" id="brightnessSlider" min="50" max="200" value="100">
<span id="brightnessVal" class="panel-val">100%</span>
</div>
</div>
<!-- 嵌入链接面板 -->
<div id="embedPanel" class="float-panel hidden">
<div class="panel-title">生成嵌入链接 <img src="./icons/close.0fb8dc6e.png" class="panel-close" data-panel="embed" alt="关闭"></div>
<div class="panel-row">
<span>模型路径</span>
<input type="text" id="embedPath" placeholder="samples/xxx.stp 或 https://...">
</div>
<div class="panel-row">
<span>嵌入地址</span>
<input type="text" id="embedUrl" readonly>
</div>
<div class="panel-row">
<span>iframe 代码</span>
<textarea id="embedCode" rows="3" readonly></textarea>
</div>
<div class="panel-row embed-actions">
<button id="btnUploadModel" class="small-btn">上传当前模型</button>
<button id="btnCopyUrl" class="small-btn">复制链接</button>
<button id="btnCopyCode" class="small-btn">复制代码</button>
</div>
<div id="embedHint" class="embed-hint"></div>
</div>
<!-- 状态栏 -->
<div id="statusbar">
<span id="statusText">就绪 | 左键旋转 · 右键平移 · 滚轮缩放</span>
<span id="modelInfo" class="status-right"></span>
</div>
</div>
</div>
<!-- 零件右键菜单 -->
<div id="ctxMenu" class="hidden">
<div class="ctx-title" id="ctxMenuTitle"></div>
<div class="ctx-item" data-act="hide"><img src="./icons/view-tool-hideShow.png" alt=""><span>隐藏</span></div>
<div class="ctx-item" data-act="isolate"><img src="./icons/view-tool-conceal.png" alt=""><span>显示其他</span></div>
<div class="ctx-item" data-act="showAll"><img src="./icons/view-tool-hideShow.png" alt=""><span>全部显示</span></div>
<div class="ctx-sep"></div>
<div class="ctx-item" data-act="color"><img src="./icons/ico-set-color.png" alt=""><span>更改颜色</span></div>
<div class="ctx-item" data-act="transparent"><img src="./icons/view-tool-transparent.png" alt=""><span>透明</span></div>
<div class="ctx-sep"></div>
<div class="ctx-item" data-act="drag"><img src="./icons/view-tool-drag.png" alt=""><span>拖动</span></div>
<div class="ctx-item" data-act="reset"><img src="./icons/view-tool-singalReset.png" alt=""><span>复位</span></div>
</div>
<input type="file" id="fileInput" accept=".step,.stp,.STEP,.STP" style="display:none">
<!-- 加载遮罩(新迪同款弹跳圆点) -->
<div id="loading" class="hidden">
<div class="waiter"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>
<div id="loadingText">正在转换 STEP 模型...</div>
</div>
<!-- 拖拽提示 -->
<div id="dropOverlay" class="hidden"><div class="drop-box">松开鼠标加载 STEP 文件</div></div>
<script type="module" src="./app.js?v=5"></script>
</body>
</html>

View File

@@ -0,0 +1,215 @@
import {
Matrix4,
Object3D,
Vector2,
Vector3
} from 'three';
class CSS2DObject extends Object3D {
constructor( element = document.createElement( 'div' ) ) {
super();
this.isCSS2DObject = true;
this.element = element;
this.element.style.position = 'absolute';
this.element.style.userSelect = 'none';
this.element.setAttribute( 'draggable', false );
this.center = new Vector2( 0.5, 0.5 ); // ( 0, 0 ) is the lower left; ( 1, 1 ) is the top right
this.addEventListener( 'removed', function () {
this.traverse( function ( object ) {
if ( object.element instanceof Element && object.element.parentNode !== null ) {
object.element.parentNode.removeChild( object.element );
}
} );
} );
}
copy( source, recursive ) {
super.copy( source, recursive );
this.element = source.element.cloneNode( true );
this.center = source.center;
return this;
}
}
//
const _vector = new Vector3();
const _viewMatrix = new Matrix4();
const _viewProjectionMatrix = new Matrix4();
const _a = new Vector3();
const _b = new Vector3();
class CSS2DRenderer {
constructor( parameters = {} ) {
const _this = this;
let _width, _height;
let _widthHalf, _heightHalf;
const cache = {
objects: new WeakMap()
};
const domElement = parameters.element !== undefined ? parameters.element : document.createElement( 'div' );
domElement.style.overflow = 'hidden';
this.domElement = domElement;
this.getSize = function () {
return {
width: _width,
height: _height
};
};
this.render = function ( scene, camera ) {
if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
_viewMatrix.copy( camera.matrixWorldInverse );
_viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix );
renderObject( scene, scene, camera );
zOrder( scene );
};
this.setSize = function ( width, height ) {
_width = width;
_height = height;
_widthHalf = _width / 2;
_heightHalf = _height / 2;
domElement.style.width = width + 'px';
domElement.style.height = height + 'px';
};
function renderObject( object, scene, camera ) {
if ( object.isCSS2DObject ) {
_vector.setFromMatrixPosition( object.matrixWorld );
_vector.applyMatrix4( _viewProjectionMatrix );
const visible = ( object.visible === true ) && ( _vector.z >= - 1 && _vector.z <= 1 ) && ( object.layers.test( camera.layers ) === true );
object.element.style.display = ( visible === true ) ? '' : 'none';
if ( visible === true ) {
object.onBeforeRender( _this, scene, camera );
const element = object.element;
element.style.transform = 'translate(' + ( - 100 * object.center.x ) + '%,' + ( - 100 * object.center.y ) + '%)' + 'translate(' + ( _vector.x * _widthHalf + _widthHalf ) + 'px,' + ( - _vector.y * _heightHalf + _heightHalf ) + 'px)';
if ( element.parentNode !== domElement ) {
domElement.appendChild( element );
}
object.onAfterRender( _this, scene, camera );
}
const objectData = {
distanceToCameraSquared: getDistanceToSquared( camera, object )
};
cache.objects.set( object, objectData );
}
for ( let i = 0, l = object.children.length; i < l; i ++ ) {
renderObject( object.children[ i ], scene, camera );
}
}
function getDistanceToSquared( object1, object2 ) {
_a.setFromMatrixPosition( object1.matrixWorld );
_b.setFromMatrixPosition( object2.matrixWorld );
return _a.distanceToSquared( _b );
}
function filterAndFlatten( scene ) {
const result = [];
scene.traverse( function ( object ) {
if ( object.isCSS2DObject ) result.push( object );
} );
return result;
}
function zOrder( scene ) {
const sorted = filterAndFlatten( scene ).sort( function ( a, b ) {
if ( a.renderOrder !== b.renderOrder ) {
return b.renderOrder - a.renderOrder;
}
const distanceA = cache.objects.get( a ).distanceToCameraSquared;
const distanceB = cache.objects.get( b ).distanceToCameraSquared;
return distanceA - distanceB;
} );
const zMax = sorted.length;
for ( let i = 0, l = sorted.length; i < l; i ++ ) {
sorted[ i ].element.style.zIndex = zMax - i;
}
}
}
}
export { CSS2DObject, CSS2DRenderer };

View File

@@ -0,0 +1,221 @@
import {
EventDispatcher,
Matrix4,
Plane,
Raycaster,
Vector2,
Vector3
} from 'three';
const _plane = new Plane();
const _raycaster = new Raycaster();
const _pointer = new Vector2();
const _offset = new Vector3();
const _intersection = new Vector3();
const _worldPosition = new Vector3();
const _inverseMatrix = new Matrix4();
class DragControls extends EventDispatcher {
constructor( _objects, _camera, _domElement ) {
super();
_domElement.style.touchAction = 'none'; // disable touch scroll
let _selected = null, _hovered = null;
const _intersections = [];
//
const scope = this;
function activate() {
_domElement.addEventListener( 'pointermove', onPointerMove );
_domElement.addEventListener( 'pointerdown', onPointerDown );
_domElement.addEventListener( 'pointerup', onPointerCancel );
_domElement.addEventListener( 'pointerleave', onPointerCancel );
}
function deactivate() {
_domElement.removeEventListener( 'pointermove', onPointerMove );
_domElement.removeEventListener( 'pointerdown', onPointerDown );
_domElement.removeEventListener( 'pointerup', onPointerCancel );
_domElement.removeEventListener( 'pointerleave', onPointerCancel );
_domElement.style.cursor = '';
}
function dispose() {
deactivate();
}
function getObjects() {
return _objects;
}
function getRaycaster() {
return _raycaster;
}
function onPointerMove( event ) {
if ( scope.enabled === false ) return;
updatePointer( event );
_raycaster.setFromCamera( _pointer, _camera );
if ( _selected ) {
if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
}
scope.dispatchEvent( { type: 'drag', object: _selected } );
return;
}
// hover support
if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
_intersections.length = 0;
_raycaster.setFromCamera( _pointer, _camera );
_raycaster.intersectObjects( _objects, scope.recursive, _intersections );
if ( _intersections.length > 0 ) {
const object = _intersections[ 0 ].object;
_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
if ( _hovered !== object && _hovered !== null ) {
scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
_domElement.style.cursor = 'auto';
_hovered = null;
}
if ( _hovered !== object ) {
scope.dispatchEvent( { type: 'hoveron', object: object } );
_domElement.style.cursor = 'pointer';
_hovered = object;
}
} else {
if ( _hovered !== null ) {
scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
_domElement.style.cursor = 'auto';
_hovered = null;
}
}
}
}
function onPointerDown( event ) {
if ( scope.enabled === false ) return;
updatePointer( event );
_intersections.length = 0;
_raycaster.setFromCamera( _pointer, _camera );
_raycaster.intersectObjects( _objects, scope.recursive, _intersections );
if ( _intersections.length > 0 ) {
_selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object;
_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
_inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
_offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
}
_domElement.style.cursor = 'move';
scope.dispatchEvent( { type: 'dragstart', object: _selected } );
}
}
function onPointerCancel() {
if ( scope.enabled === false ) return;
if ( _selected ) {
scope.dispatchEvent( { type: 'dragend', object: _selected } );
_selected = null;
}
_domElement.style.cursor = _hovered ? 'pointer' : 'auto';
}
function updatePointer( event ) {
const rect = _domElement.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
}
activate();
// API
this.enabled = true;
this.recursive = true;
this.transformGroup = false;
this.activate = activate;
this.deactivate = deactivate;
this.dispose = dispose;
this.getObjects = getObjects;
this.getRaycaster = getRaycaster;
}
}
export { DragControls };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
/**
* https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts
*/
import {
BackSide,
BoxGeometry,
Mesh,
MeshBasicMaterial,
MeshStandardMaterial,
PointLight,
Scene,
} from 'three';
class RoomEnvironment extends Scene {
constructor( renderer = null ) {
super();
const geometry = new BoxGeometry();
geometry.deleteAttribute( 'uv' );
const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
const boxMaterial = new MeshStandardMaterial();
let intensity = 5;
if ( renderer !== null && renderer._useLegacyLights === false ) intensity = 900;
const mainLight = new PointLight( 0xffffff, intensity, 28, 2 );
mainLight.position.set( 0.418, 16.199, 0.300 );
this.add( mainLight );
const room = new Mesh( geometry, roomMaterial );
room.position.set( - 0.757, 13.219, 0.717 );
room.scale.set( 31.713, 28.305, 28.591 );
this.add( room );
const box1 = new Mesh( geometry, boxMaterial );
box1.position.set( - 10.906, 2.009, 1.846 );
box1.rotation.set( 0, - 0.195, 0 );
box1.scale.set( 2.328, 7.905, 4.651 );
this.add( box1 );
const box2 = new Mesh( geometry, boxMaterial );
box2.position.set( - 5.607, - 0.754, - 0.758 );
box2.rotation.set( 0, 0.994, 0 );
box2.scale.set( 1.970, 1.534, 3.955 );
this.add( box2 );
const box3 = new Mesh( geometry, boxMaterial );
box3.position.set( 6.167, 0.857, 7.803 );
box3.rotation.set( 0, 0.561, 0 );
box3.scale.set( 3.927, 6.285, 3.687 );
this.add( box3 );
const box4 = new Mesh( geometry, boxMaterial );
box4.position.set( - 2.017, 0.018, 6.124 );
box4.rotation.set( 0, 0.333, 0 );
box4.scale.set( 2.002, 4.566, 2.064 );
this.add( box4 );
const box5 = new Mesh( geometry, boxMaterial );
box5.position.set( 2.291, - 0.756, - 2.621 );
box5.rotation.set( 0, - 0.286, 0 );
box5.scale.set( 1.546, 1.552, 1.496 );
this.add( box5 );
const box6 = new Mesh( geometry, boxMaterial );
box6.position.set( - 2.193, - 0.369, - 5.547 );
box6.rotation.set( 0, 0.516, 0 );
box6.scale.set( 3.875, 3.487, 2.986 );
this.add( box6 );
// -x right
const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
light1.position.set( - 16.116, 14.37, 8.208 );
light1.scale.set( 0.1, 2.428, 2.739 );
this.add( light1 );
// -x left
const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
light2.position.set( - 16.109, 18.021, - 8.207 );
light2.scale.set( 0.1, 2.425, 2.751 );
this.add( light2 );
// +x
const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );
light3.position.set( 14.904, 12.198, - 1.832 );
light3.scale.set( 0.15, 4.265, 6.331 );
this.add( light3 );
// +z
const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );
light4.position.set( - 0.462, 8.89, 14.520 );
light4.scale.set( 4.38, 5.441, 0.088 );
this.add( light4 );
// -z
const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );
light5.position.set( 3.235, 11.486, - 12.541 );
light5.scale.set( 2.5, 2.0, 0.1 );
this.add( light5 );
// +y
const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );
light6.position.set( 0.0, 20.0, 0.0 );
light6.scale.set( 1.0, 0.1, 1.0 );
this.add( light6 );
}
dispose() {
const resources = new Set();
this.traverse( ( object ) => {
if ( object.isMesh ) {
resources.add( object.geometry );
resources.add( object.material );
}
} );
for ( const resource of resources ) {
resource.dispose();
}
}
}
function createAreaLightMaterial( intensity ) {
const material = new MeshBasicMaterial();
material.color.setScalar( intensity );
return material;
}
export { RoomEnvironment };

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,313 @@
/* ===== 全局 ===== */
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0; height: 100%; overflow: hidden;
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
font-size: 13px; color: #333;
}
/* 嵌入模式:隐藏顶部菜单栏 */
body.embed #topbar { display: none; }
body.embed #main { top: 0; }
/* 精简菜单 (menu=min, 欧霓博目录嵌入专用): 只保留 剖切/线框/透明/视角/测量, 并隐藏模型树 */
body.menu-min #treePanel { display: none; }
body.menu-min #viewport { left: 0; }
body.menu-min .tool-btn[data-tool="select"],
body.menu-min .tool-btn[data-tool="drag"],
body.menu-min .tool-btn[data-tool="singalDrag"],
body.menu-min .tool-btn[data-tool="explode"],
body.menu-min #btnTree,
body.menu-min #btnColorPanel,
body.menu-min #btnShowAll,
body.menu-min #btnBbox,
body.menu-min #btnClear,
body.menu-min #btnSingleReset,
body.menu-min #btnAllReset,
body.menu-min #btnAxes,
body.menu-min #btnFull { display: none; }
.hidden { display: none !important; }
img { -webkit-user-drag: none; user-select: none; }
/* ===== 顶栏 ===== */
#topbar {
position: absolute; top: 0; left: 0; right: 0; height: 50px;
background: #fff; border-bottom: 1px solid #e3e6e8;
display: flex; align-items: center; gap: 14px; padding: 0 14px; z-index: 50;
}
.logo { display: flex; align-items: center; gap: 8px; }
.logo-text { font-size: 16px; font-weight: 600; color: #2b9ae8; letter-spacing: 1px; }
.btn-primary {
background: #2b9ae8; color: #fff; border: none; border-radius: 4px;
padding: 7px 18px; font-size: 13px; cursor: pointer;
}
.btn-primary:hover { background: #2389cf; }
.btn-secondary {
background: #fff; color: #2b9ae8; border: 1px solid #2b9ae8; border-radius: 4px;
padding: 6px 16px; font-size: 13px; cursor: pointer;
}
.btn-secondary:hover { background: #f0f7fd; }
.file-name { color: #888; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.quality-label { display: flex; align-items: center; gap: 6px; color: #666; font-size: 12px; flex: none; }
.quality-label select {
border: 1px solid #ddd; border-radius: 4px; padding: 4px 6px; font-size: 12px;
color: #333; background: #fff; cursor: pointer;
}
.top-right { display: flex; gap: 6px; }
.icon-btn {
width: 32px; height: 32px; border: 1px solid #ddd; border-radius: 4px;
background: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0;
}
.icon-btn:hover { border-color: #2b9ae8; }
.icon-btn img { width: 18px; height: 18px; }
/* ===== 主区域 ===== */
#main { position: absolute; top: 50px; left: 0; right: 0; bottom: 0; }
/* ===== 左侧模型树 ===== */
#treePanel {
position: absolute; top: 0; left: 0; bottom: 0; width: 280px;
background: #fff; border-right: 1px solid #e3e6e8; z-index: 40;
display: flex; flex-direction: column;
}
.tree-header { padding: 10px 12px 8px; border-bottom: 1px solid #f0f2f3; }
.tree-title { font-weight: 600; font-size: 14px; }
.tree-search {
margin-top: 8px; display: flex; align-items: center; gap: 6px;
border: 1px solid #ddd; border-radius: 4px; padding: 4px 8px;
}
.tree-search img { width: 14px; height: 14px; opacity: .6; }
.tree-search input { border: none; outline: none; flex: 1; font-size: 12px; background: transparent; }
.tree-body { flex: 1; overflow: auto; padding: 4px 0 20px; }
.tree-empty { color: #aaa; text-align: center; padding: 40px 10px; font-size: 12px; }
.tree-row {
display: flex; align-items: center; gap: 4px; height: 26px; padding: 0 6px;
cursor: pointer; white-space: nowrap; user-select: none;
}
.tree-row:hover { background: #f0f7fd; }
.tree-row.highlight { background: #dceefb; }
.tree-expander { width: 14px; height: 14px; display: inline-flex; align-items: center; justify-content: center; flex: none; }
.tree-expander img { width: 10px; height: 10px; }
.tree-spacer { width: 14px; flex: none; }
.tree-icon { width: 16px; height: 16px; flex: none; }
.tree-check { width: 14px; height: 14px; flex: none; cursor: pointer; }
.tree-label { overflow: hidden; text-overflow: ellipsis; font-size: 12px; }
.tree-row.matched .tree-label { color: #2b9ae8; font-weight: 600; }
.tree-children { display: block; }
/* ===== 视口 ===== */
#viewport { position: absolute; top: 0; left: 280px; right: 0; bottom: 0; }
#canvasWrap { position: absolute; inset: 0; }
#canvasWrap canvas { display: block; }
/* ===== 底部工具栏 ===== */
#toolbar {
position: absolute; left: 50%; bottom: 36px; transform: translateX(-50%);
display: flex; flex-direction: row; gap: 2px; z-index: 45;
background: #fff; border: 1px solid #e3e6e8; border-radius: 6px;
padding: 4px 8px; box-shadow: 0 2px 10px rgba(0,0,0,.08);
}
.tool-btn {
width: 72px; height: 72px; border: none; background: transparent; border-radius: 4px;
cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 4px; color: #666; padding: 0;
}
.tool-btn img { width: 40px; height: 40px; }
.tool-btn span { font-size: 12px; line-height: 1; }
.tool-btn:hover { background: #f0f7fd; color: #2b9ae8; }
.tool-btn.active { background: #2b9ae8; color: #fff; }
.tool-btn.active img { filter: brightness(0) invert(1); }
.tool-sep { height: 1px; background: #eee; margin: 2px 4px; }
/* ===== 浮动面板 ===== */
.float-panel {
position: absolute; z-index: 46; background: #fff; border: 1px solid #e3e6e8;
border-radius: 6px; box-shadow: 0 2px 12px rgba(0,0,0,.12); padding: 10px 12px; width: 240px;
}
#explodePanel { right: 16px; top: 20px; }
#sectionPanel { right: 16px; top: 120px; }
.panel-title {
font-weight: 600; font-size: 13px; margin-bottom: 10px;
display: flex; justify-content: space-between; align-items: center;
}
.panel-close { width: 14px; height: 14px; cursor: pointer; opacity: .6; }
.panel-close:hover { opacity: 1; }
.panel-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.panel-row:last-child { margin-bottom: 0; }
.panel-row span { font-size: 12px; color: #666; flex: none; }
.panel-row input[type="range"] { flex: 1; accent-color: #2b9ae8; }
.panel-val { width: 44px; text-align: right; color: #2b9ae8; font-weight: 600; }
.axis-btn {
width: 28px; height: 24px; border: 1px solid #ccc; background: #fff; border-radius: 3px;
cursor: pointer; font-weight: 700; color: #555;
}
.axis-btn.active { background: #2b9ae8; border-color: #2b9ae8; color: #fff; }
.small-btn {
border: 1px solid #ccc; background: #fff; border-radius: 3px; cursor: pointer;
font-size: 11px; padding: 4px 8px; color: #555;
}
.small-btn:hover { border-color: #2b9ae8; color: #2b9ae8; }
.small-btn.on { background: #2b9ae8; border-color: #2b9ae8; color: #fff; }
/* ===== 测量面板(水平单行) ===== */
.measure-panel {
left: 50%; bottom: 36px; transform: translateX(-50%);
display: flex; align-items: center; gap: 2px; padding: 6px 8px;
width: auto; max-width: 96vw; overflow-x: auto;
}
#measureModes { display: flex; gap: 2px; flex-wrap: nowrap; }
.measure-btn {
display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 72px; height: 72px; padding: 0; border: 1px solid transparent; border-radius: 4px;
background: transparent; cursor: pointer; color: #555; flex: none;
}
.measure-btn img { width: 40px; height: 40px; }
.measure-btn span { display: none; }
.measure-btn:hover { background: #f0f7fd; }
.measure-btn.active { background: #2b9ae8; color: #fff; }
.measure-btn.active img { filter: brightness(0) invert(1); }
.measure-actions {
display: flex; gap: 2px; margin-left: 6px; padding-left: 6px;
border-left: 1px solid #eee; flex: none;
}
.measure-act {
width: 72px; height: 72px; border: none; background: transparent; border-radius: 4px;
cursor: pointer; display: flex; align-items: center; justify-content: center; flex: none;
}
.measure-act:hover { background: #f0f7fd; }
.measure-act img { width: 40px; height: 40px; }
/* ===== 结果统计面板 ===== */
#statsPanel { right: 16px; top: 220px; width: 260px; max-height: 320px; display: flex; flex-direction: column; }
#statsPanel .panel-title { flex: none; }
#statsList { overflow: auto; flex: 1; }
.stats-empty { color: #aaa; text-align: center; padding: 16px 0; font-size: 12px; }
.stats-row {
display: flex; align-items: center; gap: 6px; padding: 5px 2px;
border-bottom: 1px solid #f5f6f7; font-size: 12px;
}
.stats-row:last-child { border-bottom: none; }
.stats-type { color: #2b9ae8; flex: none; font-weight: 600; }
.stats-value { flex: 1; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stats-del { cursor: pointer; color: #bbb; flex: none; padding: 0 4px; }
.stats-del:hover { color: #e74c3c; }
/* ===== 上色面板 ===== */
#colorPanel { right: 16px; top: 20px; width: 230px; }
#colorPicker { width: 36px; height: 28px; border: 1px solid #ddd; border-radius: 4px; padding: 1px; cursor: pointer; }
#colorPanel .small-btn { display: flex; align-items: center; gap: 4px; }
#colorPanel .small-btn img { width: 14px; height: 14px; }
/* ===== 嵌入链接面板 ===== */
#embedPanel { right: 16px; top: 20px; width: 340px; }
#embedPanel .panel-row { align-items: flex-start; flex-wrap: wrap; }
#embedPanel .panel-row > span { padding-top: 6px; }
#embedPanel input[type="text"], #embedPanel textarea {
flex: 1; border: 1px solid #ddd; border-radius: 4px; padding: 5px 8px;
font-size: 11px; color: #444; min-width: 200px;
}
#embedPanel textarea { resize: vertical; font-family: Consolas, monospace; }
.embed-actions { justify-content: flex-end; }
.embed-hint { font-size: 11px; color: #999; line-height: 1.6; }
/* ===== 零件右键菜单 ===== */
#ctxMenu {
position: fixed; z-index: 500; background: #fff; border: 1px solid #e3e6e8;
border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,.15); padding: 6px 0;
min-width: 140px;
}
.ctx-title {
padding: 6px 14px; font-weight: 600; font-size: 12px; color: #2b9ae8;
border-bottom: 1px solid #f0f2f3; margin-bottom: 4px;
max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ctx-item {
display: flex; align-items: center; gap: 8px; padding: 7px 14px;
cursor: pointer; font-size: 12px; color: #444;
}
.ctx-item:hover { background: #f0f7fd; color: #2b9ae8; }
.ctx-item img { width: 18px; height: 18px; }
.ctx-sep { height: 1px; background: #f0f2f3; margin: 4px 0; }
/* ===== 状态栏 ===== */
#statusbar {
position: absolute; left: 0; right: 0; bottom: 0; height: 26px;
background: rgba(255,255,255,.92); border-top: 1px solid #e3e6e8;
display: flex; align-items: center; justify-content: space-between;
padding: 0 12px; font-size: 11px; color: #888; z-index: 44;
}
.status-right { color: #2b9ae8; }
/* ===== 加载遮罩(新迪同款弹跳圆点) ===== */
#loading {
position: fixed; inset: 0; background: rgba(255,255,255,.9); z-index: 200;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;
}
.waiter { display: flex; gap: 12px; }
.waiter > div {
width: 18px; height: 18px; background-color: #2b9ae8; border-radius: 100%;
animation: bouncedelay 1.4s infinite ease-in-out; animation-fill-mode: both;
}
.waiter .bounce1 { animation-delay: -0.75s; }
.waiter .bounce2 { animation-delay: -0.5s; }
.waiter .bounce3 { animation-delay: -0.25s; }
@keyframes bouncedelay {
0% { transform: scale(0); }
50% { transform: scale(1); }
100% { transform: scale(0); }
}
#loadingText { color: #666; font-size: 14px; }
/* ===== 拖拽遮罩 ===== */
#dropOverlay {
position: fixed; inset: 0; background: rgba(43,154,232,.08); z-index: 300;
display: flex; align-items: center; justify-content: center;
}
.drop-box {
border: 2px dashed #2b9ae8; border-radius: 12px; background: #fff;
padding: 50px 80px; font-size: 18px; color: #2b9ae8; pointer-events: none;
}
/* ===== 测量标注 ===== */
.measure-label {
background: #2b9ae8; color: #fff; font-size: 12px; font-weight: 600;
padding: 3px 8px; border-radius: 3px; white-space: nowrap; pointer-events: none;
box-shadow: 0 1px 4px rgba(0,0,0,.3);
}
.measure-label::after {
content: ''; position: absolute; left: 50%; top: 100%; margin-left: -4px;
border: 4px solid transparent; border-top-color: #2b9ae8;
}
/* ===== 手机适配≤768px ===== */
.tree-close { display: none; }
@media (max-width: 768px) {
.tree-close {
display: block; position: absolute; top: 12px; right: 10px;
border: none; background: #f2f7fc; color: #6b8498; font-size: 14px;
border-radius: 4px; padding: 2px 8px; cursor: pointer;
}
.tree-close:hover { background: #2b9ae8; color: #fff; }
#topbar { height: 44px; gap: 8px; padding: 0 8px; }
#topbar .logo-text { display: none; }
#topbar .file-name { display: none; }
#topbar .btn-primary, #topbar .btn-secondary { padding: 5px 10px; font-size: 12px; }
#main { top: 44px; }
body.embed #main { top: 0; }
/* 模型树抽屉:默认隐藏,点「结构树」按钮弹出 */
#treePanel {
position: fixed; top: 44px; left: 0; bottom: 0; width: min(280px, 85vw); z-index: 60;
transform: translateX(-100%); transition: transform .2s ease; box-shadow: 2px 0 12px rgba(0,0,0,.15);
}
body.embed #treePanel { top: 0; }
body.tree-open #treePanel { transform: translateX(0); }
#treePanel.hidden { display: flex; } /* 手机上抽屉由 tree-open 控制 */
#viewport { left: 0; }
#toolbar { left: 0; right: 0; transform: none; bottom: 8px;
border-radius: 0; border-left: none; border-right: none; padding: 2px 4px; overflow-x: auto; }
.tool-btn { width: 56px; height: 60px; flex: none; }
.tool-btn img { width: 32px; height: 32px; }
.tool-btn span { font-size: 11px; }
.float-panel { max-width: 92vw; max-height: 70vh; overflow: auto; }
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

1408
release/web_2026.08/app.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
// 站点配置 (部署时按需修改): NX 按需生成服务地址
// api 为空 = 非标档位下载按钮维持置灰态 (一期行为); 填上服务器地址后按钮升级为【在线生成数模】
// 例: window.GEN_CONFIG = { api: "https://gen.example.com" };
window.GEN_CONFIG = { api: "http://localhost:8899" };

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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