Files
temperature/cad/viewer.html
DESKTOP-PUHFO60\dsx2016 4898087dc7 Add browser Three.js viewer for case STL preview.
Serve with python http.server under cad/ and open viewer.html locally.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 15:01:24 +08:00

226 lines
7.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CYD Climate Case — 3D Preview</title>
<style>
:root {
--bg: #0b1220;
--panel: #152238;
--text: #e8eef7;
--muted: #8a9bb5;
--accent: #3db8a8;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text);
font-family: "Segoe UI", system-ui, sans-serif; overflow: hidden; }
#c { display: block; width: 100%; height: 100%; }
.hud {
position: fixed; left: 16px; top: 16px; z-index: 2;
background: color-mix(in srgb, var(--panel) 92%, transparent);
border: 1px solid #243247; border-radius: 12px; padding: 14px 16px;
max-width: min(340px, calc(100vw - 32px)); backdrop-filter: blur(8px);
}
.hud h1 { margin: 0 0 6px; font-size: 16px; font-weight: 650; letter-spacing: 0.02em; }
.hud p { margin: 0 0 10px; font-size: 12px; color: var(--muted); line-height: 1.45; }
.row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
button, label.tog {
appearance: none; border: 1px solid #2c3d56; background: #1a2740; color: var(--text);
border-radius: 8px; padding: 6px 10px; font-size: 12px; cursor: pointer;
}
button:hover, label.tog:hover { border-color: var(--accent); }
button.active { background: #1e3d3a; border-color: var(--accent); color: #b6f0e6; }
label.tog { display: inline-flex; align-items: center; gap: 6px; user-select: none; }
#status { margin-top: 10px; font-size: 11px; color: var(--muted); }
#status.err { color: #e05a5a; }
</style>
</head>
<body>
<div class="hud">
<h1>CYD Climate Case</h1>
<p>拖拽旋转 · 滚轮缩放 · 右键平移</p>
<div class="row">
<button type="button" id="btn-both" class="active">整体</button>
<button type="button" id="btn-front">仅前框</button>
<button type="button" id="btn-back">仅后盖</button>
<button type="button" id="btn-explode">分解 / 合拢</button>
</div>
<div class="row">
<label class="tog"><input type="checkbox" id="chk-grid" checked /> 网格</label>
<label class="tog"><input type="checkbox" id="chk-auto" checked /> 自转</label>
<button type="button" id="btn-reset">复位视角</button>
</div>
<div id="status">加载中…</div>
</div>
<canvas id="c"></canvas>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.170.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.170.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { STLLoader } from "three/addons/loaders/STLLoader.js";
const statusEl = document.getElementById("status");
const canvas = document.getElementById("c");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight, false);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b1220);
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 2000);
camera.position.set(120, 90, 140);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.target.set(45, 10, 20);
scene.add(new THREE.AmbientLight(0xffffff, 0.55));
const key = new THREE.DirectionalLight(0xffffff, 1.05);
key.position.set(80, 120, 60);
scene.add(key);
const fill = new THREE.DirectionalLight(0x88aacc, 0.45);
fill.position.set(-60, 40, -40);
scene.add(fill);
const grid = new THREE.GridHelper(200, 20, 0x3db8a8, 0x1e2c40);
grid.position.y = -20;
scene.add(grid);
const root = new THREE.Group();
scene.add(root);
const matFront = new THREE.MeshStandardMaterial({
color: 0x5a6a80, metalness: 0.15, roughness: 0.55, flatShading: true,
});
const matBack = new THREE.MeshStandardMaterial({
color: 0x3db8a8, metalness: 0.12, roughness: 0.5, flatShading: true,
});
let meshFront = null;
let meshBack = null;
let exploded = false;
let autoRotate = true;
function setStatus(msg, err = false) {
statusEl.textContent = msg;
statusEl.classList.toggle("err", err);
}
function fitCamera(object) {
const box = new THREE.Box3().setFromObject(object);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
controls.target.copy(center);
const maxDim = Math.max(size.x, size.y, size.z);
const dist = maxDim * 1.8;
camera.position.set(center.x + dist * 0.7, center.y + dist * 0.45, center.z + dist * 0.7);
camera.near = maxDim / 100;
camera.far = maxDim * 50;
camera.updateProjectionMatrix();
controls.update();
}
const loader = new STLLoader();
function loadSTL(url, material) {
return new Promise((resolve, reject) => {
loader.load(
url,
(geometry) => {
geometry.computeVertexNormals();
const mesh = new THREE.Mesh(geometry, material);
// STL is in mm; keep 1 unit = 1 mm
resolve(mesh);
},
undefined,
reject
);
});
}
async function boot() {
try {
const [front, back] = await Promise.all([
loadSTL("./export/front.stl", matFront),
loadSTL("./export/back.stl", matBack),
]);
meshFront = front;
meshBack = back;
// Lay parts for a readable assembly preview:
// back on "desk", front slightly above / in front
meshBack.rotation.x = -Math.PI / 2;
meshFront.rotation.x = -Math.PI / 2;
meshFront.position.y = 12;
root.add(meshBack);
root.add(meshFront);
fitCamera(root);
setStatus("已加载 front.stl + back.stl(单位 mm)");
} catch (e) {
console.error(e);
setStatus(
"加载失败:请用本地 HTTP 打开(不要双击 file://)。在项目根目录运行: python -m http.server 8765",
true
);
}
}
function setMode(mode) {
document.querySelectorAll(".row button").forEach((b) => b.classList.remove("active"));
if (mode === "both") document.getElementById("btn-both").classList.add("active");
if (mode === "front") document.getElementById("btn-front").classList.add("active");
if (mode === "back") document.getElementById("btn-back").classList.add("active");
if (meshFront) meshFront.visible = mode !== "back";
if (meshBack) meshBack.visible = mode !== "front";
}
document.getElementById("btn-both").onclick = () => setMode("both");
document.getElementById("btn-front").onclick = () => setMode("front");
document.getElementById("btn-back").onclick = () => setMode("back");
document.getElementById("btn-explode").onclick = () => {
exploded = !exploded;
if (meshFront) meshFront.position.y = exploded ? 28 : 12;
if (meshBack) meshBack.position.y = exploded ? -8 : 0;
};
document.getElementById("chk-grid").onchange = (e) => {
grid.visible = e.target.checked;
};
document.getElementById("chk-auto").onchange = (e) => {
autoRotate = e.target.checked;
};
document.getElementById("btn-reset").onclick = () => {
if (root.children.length) fitCamera(root);
};
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight, false);
});
function tick() {
requestAnimationFrame(tick);
if (autoRotate) root.rotation.y += 0.004;
controls.update();
renderer.render(scene, camera);
}
boot();
tick();
</script>
</body>
</html>