增强曲柄滑块三维验证视图
This commit is contained in:
@@ -299,6 +299,82 @@
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.three-stage {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0f1720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #17212b;
|
||||||
|
color: #edf6fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-toolbar h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-toolbar .secondary {
|
||||||
|
min-height: 38px;
|
||||||
|
background: #1f2d3a;
|
||||||
|
border-color: #3d4c5a;
|
||||||
|
color: #edf6fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-viewport {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-viewport canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
bottom: 14px;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
width: min(360px, calc(100% - 28px));
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(237, 246, 251, 0.16);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(15, 23, 32, 0.78);
|
||||||
|
color: #edf6fb;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-overlay strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-note {
|
||||||
|
padding: 12px 14px 14px;
|
||||||
|
border-top: 1px solid rgba(237, 246, 251, 0.12);
|
||||||
|
color: #c8d3dc;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-card {
|
.chart-card {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -441,6 +517,15 @@
|
|||||||
.chart-grid {
|
.chart-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.three-toolbar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-viewport {
|
||||||
|
height: 380px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
@@ -543,6 +628,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="three-stage" aria-label="曲柄滑块 3D 验证视图">
|
||||||
|
<div class="three-toolbar">
|
||||||
|
<h3>Three.js 机构空间视图</h3>
|
||||||
|
<div class="three-toolbar-actions">
|
||||||
|
<button class="secondary" id="playScanButton" type="button">播放扫描</button>
|
||||||
|
<button class="secondary" id="pauseScanButton" type="button">暂停</button>
|
||||||
|
<button class="secondary" id="resetCameraButton" type="button">复位视角</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="three-viewport" id="threeViewport">
|
||||||
|
<div class="three-overlay" id="threeOverlay">
|
||||||
|
<strong>等待接口数据</strong>
|
||||||
|
<span>调用接口后显示当前 A-B-S 姿态,角度扫描后可播放 WASM 返回的连续点位。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="three-note">
|
||||||
|
<strong>3D 视图</strong>
|
||||||
|
使用接口返回的 A、B、S 点直接驱动模型:蓝色为曲柄 AB,金色为连杆 BS,绿色为滑块,灰色为导轨。播放扫描时不会重新计算几何,只复用多次接口调用得到的点位序列。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="chart-grid" aria-label="曲柄滑块图形结果">
|
<div class="chart-grid" aria-label="曲柄滑块图形结果">
|
||||||
<div class="chart-card wide">
|
<div class="chart-card wide">
|
||||||
<div class="chart-title">
|
<div class="chart-title">
|
||||||
@@ -610,6 +716,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/three@0.160.1/build/three.min.js"></script>
|
||||||
<script src="./wasm/smart_math.js"></script>
|
<script src="./wasm/smart_math.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const validSample = {
|
const validSample = {
|
||||||
@@ -628,8 +735,15 @@
|
|||||||
|
|
||||||
let wasmModule = null;
|
let wasmModule = null;
|
||||||
let charts = null;
|
let charts = null;
|
||||||
|
let threeView = null;
|
||||||
let latestResponse = null;
|
let latestResponse = null;
|
||||||
let latestScan = [];
|
let latestScan = [];
|
||||||
|
let scanAnimation = {
|
||||||
|
active: false,
|
||||||
|
frame: 0,
|
||||||
|
timer: 0,
|
||||||
|
intervalId: null,
|
||||||
|
};
|
||||||
|
|
||||||
const wasmStatus = document.getElementById("wasmStatus");
|
const wasmStatus = document.getElementById("wasmStatus");
|
||||||
const requestStatus = document.getElementById("requestStatus");
|
const requestStatus = document.getElementById("requestStatus");
|
||||||
@@ -651,6 +765,8 @@
|
|||||||
const scanOutput = document.getElementById("scanOutput");
|
const scanOutput = document.getElementById("scanOutput");
|
||||||
const responseTab = document.getElementById("responseTab");
|
const responseTab = document.getElementById("responseTab");
|
||||||
const scanTab = document.getElementById("scanTab");
|
const scanTab = document.getElementById("scanTab");
|
||||||
|
const threeViewport = document.getElementById("threeViewport");
|
||||||
|
const threeOverlay = document.getElementById("threeOverlay");
|
||||||
|
|
||||||
const chartElements = {
|
const chartElements = {
|
||||||
mechanism: document.getElementById("mechanismChart"),
|
mechanism: document.getElementById("mechanismChart"),
|
||||||
@@ -742,6 +858,214 @@
|
|||||||
return charts;
|
return charts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化 Three.js 场景,负责更直观地展示接口返回的机构姿态。
|
||||||
|
function ensureThreeView() {
|
||||||
|
if (!window.THREE) {
|
||||||
|
throw new Error("Three.js 加载失败,请检查网络或 CDN 可用性");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threeView) return threeView;
|
||||||
|
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x0f1720);
|
||||||
|
|
||||||
|
const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 100);
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
threeViewport.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
const ambient = new THREE.AmbientLight(0xffffff, 0.58);
|
||||||
|
const keyLight = new THREE.DirectionalLight(0xffffff, 0.9);
|
||||||
|
keyLight.position.set(3, 4, 5);
|
||||||
|
scene.add(ambient, keyLight);
|
||||||
|
|
||||||
|
const grid = new THREE.GridHelper(8, 32, 0x46606f, 0x263744);
|
||||||
|
grid.position.y = -0.08;
|
||||||
|
scene.add(grid);
|
||||||
|
|
||||||
|
const group = new THREE.Group();
|
||||||
|
scene.add(group);
|
||||||
|
|
||||||
|
const materials = {
|
||||||
|
crank: new THREE.MeshStandardMaterial({ color: 0x0f6f8f, roughness: 0.42, metalness: 0.18 }),
|
||||||
|
rod: new THREE.MeshStandardMaterial({ color: 0xc18a18, roughness: 0.46, metalness: 0.12 }),
|
||||||
|
slider: new THREE.MeshStandardMaterial({ color: 0x24a267, roughness: 0.5, metalness: 0.08 }),
|
||||||
|
joint: new THREE.MeshStandardMaterial({ color: 0xf3f7fa, roughness: 0.35, metalness: 0.18 }),
|
||||||
|
rail: new THREE.MeshStandardMaterial({ color: 0x768898, roughness: 0.58, metalness: 0.08 }),
|
||||||
|
path: new THREE.LineBasicMaterial({ color: 0x62c7e8 }),
|
||||||
|
sliderPath: new THREE.LineBasicMaterial({ color: 0x8ddf9e }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const crank = createBar(materials.crank);
|
||||||
|
const rod = createBar(materials.rod);
|
||||||
|
const rail = createBar(materials.rail, 0.035);
|
||||||
|
const slider = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.16, 0.18), materials.slider);
|
||||||
|
const pointA = createJoint(materials.joint, 0.075);
|
||||||
|
const pointB = createJoint(materials.joint, 0.065);
|
||||||
|
const pointS = createJoint(materials.joint, 0.07);
|
||||||
|
const pathLine = new THREE.Line(new THREE.BufferGeometry(), materials.path);
|
||||||
|
const sliderPathLine = new THREE.Line(new THREE.BufferGeometry(), materials.sliderPath);
|
||||||
|
|
||||||
|
group.add(crank, rod, rail, slider, pointA, pointB, pointS, pathLine, sliderPathLine);
|
||||||
|
|
||||||
|
threeView = {
|
||||||
|
scene,
|
||||||
|
camera,
|
||||||
|
renderer,
|
||||||
|
group,
|
||||||
|
objects: { crank, rod, rail, slider, pointA, pointB, pointS, pathLine, sliderPathLine },
|
||||||
|
};
|
||||||
|
|
||||||
|
resetThreeCamera();
|
||||||
|
resizeThreeView();
|
||||||
|
window.addEventListener("resize", resizeThreeView);
|
||||||
|
renderer.setAnimationLoop(renderThreeFrame);
|
||||||
|
return threeView;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一根沿 X 轴缩放的圆柱连杆。
|
||||||
|
function createBar(material, radius = 0.045) {
|
||||||
|
const geometry = new THREE.CylinderGeometry(radius, radius, 1, 24);
|
||||||
|
geometry.rotateZ(Math.PI / 2);
|
||||||
|
return new THREE.Mesh(geometry, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建关键铰点的小球。
|
||||||
|
function createJoint(material, radius) {
|
||||||
|
return new THREE.Mesh(new THREE.SphereGeometry(radius, 24, 16), material);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把二维接口点映射到 Three.js 的 XZ 平面。
|
||||||
|
function toThreePoint(point) {
|
||||||
|
return new THREE.Vector3(point.x, 0, point.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新圆柱连杆的位置、长度和朝向。
|
||||||
|
function placeBar(mesh, start, end) {
|
||||||
|
const from = toThreePoint(start);
|
||||||
|
const to = toThreePoint(end);
|
||||||
|
const delta = new THREE.Vector3().subVectors(to, from);
|
||||||
|
const length = Math.max(delta.length(), 0.000001);
|
||||||
|
mesh.position.copy(from).addScaledVector(delta, 0.5);
|
||||||
|
mesh.scale.set(length, 1, 1);
|
||||||
|
mesh.quaternion.setFromUnitVectors(new THREE.Vector3(1, 0, 0), delta.normalize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据机构范围调整相机距离,避免 3D 模型出框。
|
||||||
|
function resetThreeCamera(input = readInput()) {
|
||||||
|
const view = ensureThreeView();
|
||||||
|
const span = Math.max(input.L_AB + input.L_BS, Math.abs(input.S_OFS) + input.L_AB, 1);
|
||||||
|
view.camera.position.set(span * 0.9, span * 1.45, span * 1.75);
|
||||||
|
view.camera.lookAt(span * 0.35, 0, input.S_OFS * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整 Three.js 画布尺寸。
|
||||||
|
function resizeThreeView() {
|
||||||
|
if (!threeView) return;
|
||||||
|
const rect = threeViewport.getBoundingClientRect();
|
||||||
|
threeView.camera.aspect = Math.max(rect.width, 1) / Math.max(rect.height, 1);
|
||||||
|
threeView.camera.updateProjectionMatrix();
|
||||||
|
threeView.renderer.setSize(rect.width, rect.height, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Three.js 持续渲染循环。
|
||||||
|
function renderThreeFrame() {
|
||||||
|
if (!threeView) return;
|
||||||
|
threeView.renderer.render(threeView.scene, threeView.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用接口点位刷新 3D 机构姿态。
|
||||||
|
function renderThreeMechanism(points, input, meta = {}) {
|
||||||
|
const view = ensureThreeView();
|
||||||
|
const a = points.A || points.pointA;
|
||||||
|
const b = points.B || points.pointB;
|
||||||
|
const s = points.S || points.pointS;
|
||||||
|
const railHalfLength = Math.max(input.L_AB + input.L_BS + Math.abs(input.S_OFS), Math.abs(s.x) + 0.8, 1);
|
||||||
|
|
||||||
|
placeBar(view.objects.crank, a, b);
|
||||||
|
placeBar(view.objects.rod, b, s);
|
||||||
|
placeBar(
|
||||||
|
view.objects.rail,
|
||||||
|
{ x: -railHalfLength * 0.25, y: input.S_OFS },
|
||||||
|
{ x: railHalfLength, y: input.S_OFS },
|
||||||
|
);
|
||||||
|
|
||||||
|
view.objects.pointA.position.copy(toThreePoint(a));
|
||||||
|
view.objects.pointB.position.copy(toThreePoint(b));
|
||||||
|
view.objects.pointS.position.copy(toThreePoint(s));
|
||||||
|
view.objects.slider.position.copy(toThreePoint(s));
|
||||||
|
view.objects.slider.position.y = 0.02;
|
||||||
|
|
||||||
|
if (meta.fitCamera) {
|
||||||
|
resetThreeCamera(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
threeOverlay.innerHTML = `
|
||||||
|
<strong>${meta.title || "当前接口姿态"}</strong>
|
||||||
|
<span>angle=${fmt(meta.angle ?? input.angleDeg, 2)}°,B=(${fmt(b.x, 4)}, ${fmt(b.y, 4)}),S=(${fmt(s.x, 4)}, ${fmt(s.y, 4)})</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用扫描结果刷新 3D 轨迹线。
|
||||||
|
function renderThreePaths(scanRows) {
|
||||||
|
const view = ensureThreeView();
|
||||||
|
const validRows = scanRows.filter((row) => row.ok);
|
||||||
|
const bPoints = validRows.map((row) => toThreePoint(row.B));
|
||||||
|
const sPoints = validRows.map((row) => toThreePoint(row.S));
|
||||||
|
|
||||||
|
view.objects.pathLine.geometry.dispose();
|
||||||
|
view.objects.pathLine.geometry = new THREE.BufferGeometry().setFromPoints(bPoints);
|
||||||
|
view.objects.sliderPathLine.geometry.dispose();
|
||||||
|
view.objects.sliderPathLine.geometry = new THREE.BufferGeometry().setFromPoints(sPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放扫描结果中的下一帧。
|
||||||
|
function animateScanFrame() {
|
||||||
|
const validRows = latestScan.filter((row) => row.ok);
|
||||||
|
if (!validRows.length) {
|
||||||
|
pauseScanAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = validRows[scanAnimation.frame % validRows.length];
|
||||||
|
const input = { ...readInput(), angleDeg: row.angle };
|
||||||
|
renderThreeMechanism(
|
||||||
|
{ A: { x: 0, y: 0 }, B: row.B, S: row.S },
|
||||||
|
input,
|
||||||
|
{ title: "扫描播放中", angle: row.angle },
|
||||||
|
);
|
||||||
|
scanAnimation.frame += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始播放已扫描的接口点位。
|
||||||
|
function playScanAnimation() {
|
||||||
|
if (!latestScan.some((row) => row.ok)) {
|
||||||
|
setMessage("请先执行角度扫描,再播放 3D 动画", "is-error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderThreePaths(latestScan);
|
||||||
|
if (scanAnimation.intervalId) {
|
||||||
|
clearInterval(scanAnimation.intervalId);
|
||||||
|
}
|
||||||
|
scanAnimation.active = true;
|
||||||
|
scanAnimation.timer = 0;
|
||||||
|
animateScanFrame();
|
||||||
|
scanAnimation.intervalId = window.setInterval(animateScanFrame, 120);
|
||||||
|
setMessage("正在播放扫描点位动画", "is-ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂停 3D 扫描动画。
|
||||||
|
function pauseScanAnimation() {
|
||||||
|
scanAnimation.active = false;
|
||||||
|
if (scanAnimation.intervalId) {
|
||||||
|
clearInterval(scanAnimation.intervalId);
|
||||||
|
scanAnimation.intervalId = null;
|
||||||
|
}
|
||||||
|
setMessage("3D 动画已暂停", "is-ok");
|
||||||
|
}
|
||||||
|
|
||||||
// 加载并初始化 WASM 模块。
|
// 加载并初始化 WASM 模块。
|
||||||
async function ensureWasm() {
|
async function ensureWasm() {
|
||||||
if (wasmModule) return wasmModule;
|
if (wasmModule) return wasmModule;
|
||||||
@@ -987,6 +1311,8 @@
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
renderThreeMechanism({ A: a, B: b, S: s }, input, { fitCamera: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染角度扫描汇总图。
|
// 渲染角度扫描汇总图。
|
||||||
@@ -1045,6 +1371,8 @@
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
renderThreePaths(scanRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行当前角度的单次接口验证。
|
// 执行当前角度的单次接口验证。
|
||||||
@@ -1061,9 +1389,11 @@
|
|||||||
renderSummary(response);
|
renderSummary(response);
|
||||||
renderChecks(response, input);
|
renderChecks(response, input);
|
||||||
if (response.res_data && response.res_data.success) {
|
if (response.res_data && response.res_data.success) {
|
||||||
|
scanAnimation.active = false;
|
||||||
renderMechanismChart(response, input);
|
renderMechanismChart(response, input);
|
||||||
setMessage("接口调用完成,当前姿态已刷新", "is-ok");
|
setMessage("接口调用完成,当前姿态已刷新", "is-ok");
|
||||||
} else {
|
} else {
|
||||||
|
scanAnimation.active = false;
|
||||||
renderMechanismChart({ res_data: { points: calculateLocalGeometry(input) } }, input);
|
renderMechanismChart({ res_data: { points: calculateLocalGeometry(input) } }, input);
|
||||||
setMessage(response.res_data?.error || response.msg || "接口返回业务失败", "is-error");
|
setMessage(response.res_data?.error || response.msg || "接口返回业务失败", "is-error");
|
||||||
}
|
}
|
||||||
@@ -1111,6 +1441,7 @@
|
|||||||
latestScan = rows;
|
latestScan = rows;
|
||||||
scanOutput.textContent = JSON.stringify(rows, null, 2);
|
scanOutput.textContent = JSON.stringify(rows, null, 2);
|
||||||
renderScanCharts(rows);
|
renderScanCharts(rows);
|
||||||
|
scanAnimation.frame = 0;
|
||||||
requestStatus.textContent = "扫描完成";
|
requestStatus.textContent = "扫描完成";
|
||||||
requestStatus.className = "badge ok";
|
requestStatus.className = "badge ok";
|
||||||
setMessage(`扫描完成:${rows.filter((row) => row.ok).length}/${rows.length} 个角度有效`, "is-ok");
|
setMessage(`扫描完成:${rows.filter((row) => row.ok).length}/${rows.length} 个角度有效`, "is-ok");
|
||||||
@@ -1146,6 +1477,9 @@
|
|||||||
|
|
||||||
document.getElementById("runButton").addEventListener("click", runOnce);
|
document.getElementById("runButton").addEventListener("click", runOnce);
|
||||||
document.getElementById("scanButton").addEventListener("click", runScan);
|
document.getElementById("scanButton").addEventListener("click", runScan);
|
||||||
|
document.getElementById("playScanButton").addEventListener("click", playScanAnimation);
|
||||||
|
document.getElementById("pauseScanButton").addEventListener("click", pauseScanAnimation);
|
||||||
|
document.getElementById("resetCameraButton").addEventListener("click", () => resetThreeCamera(readInput()));
|
||||||
responseTab.addEventListener("click", () => setActiveTab("response"));
|
responseTab.addEventListener("click", () => setActiveTab("response"));
|
||||||
scanTab.addEventListener("click", () => setActiveTab("scan"));
|
scanTab.addEventListener("click", () => setActiveTab("scan"));
|
||||||
|
|
||||||
@@ -1153,6 +1487,10 @@
|
|||||||
stepInput.value = "10";
|
stepInput.value = "10";
|
||||||
scanEndInput.value = "360";
|
scanEndInput.value = "360";
|
||||||
refreshRequestPreview();
|
refreshRequestPreview();
|
||||||
|
renderThreeMechanism(calculateLocalGeometry(validSample), validSample, {
|
||||||
|
title: "有效样例预览",
|
||||||
|
fitCamera: true,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user