1
This commit is contained in:
BIN
assets/百叶1.glb
Normal file
BIN
assets/百叶1.glb
Normal file
Binary file not shown.
BIN
assets/百叶2.glb
Normal file
BIN
assets/百叶2.glb
Normal file
Binary file not shown.
BIN
assets/百叶3.glb
Normal file
BIN
assets/百叶3.glb
Normal file
Binary file not shown.
BIN
assets/百叶4.glb
Normal file
BIN
assets/百叶4.glb
Normal file
Binary file not shown.
BIN
examples/btn_热点.png
Normal file
BIN
examples/btn_热点.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@ -62,6 +62,29 @@
|
||||
sdkKernel.on('model:click', (data) => {
|
||||
console.log('模型点击事件', data);
|
||||
});
|
||||
sdkKernel.on('all:ready', (data) => {
|
||||
console.log('所有模块加载完,', data);
|
||||
sdkKernel.material.apply({
|
||||
target: 'Material__2',
|
||||
attribute: 'alpha',
|
||||
value: 0.5,
|
||||
});
|
||||
|
||||
|
||||
sdkKernel.hotspot.render([
|
||||
{
|
||||
id: "h1",
|
||||
name: "卷帘门",
|
||||
meshName: "Valve_01",
|
||||
icon: "https://bpic.588ku.com/element_pic/20/06/30/d1046b01afc0b9586844350d131f4daf.jpg!/fw/253/quality/90/unsharp/true/compress/true",
|
||||
offset: [25, 25, 0],
|
||||
radius: 20,
|
||||
color: "#21c7ff",
|
||||
payload: { type: "valve", code: "A" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -2,70 +2,185 @@
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SDK 模块化加载示例</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D Model Showcase SDK - TS</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#renderDom {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
#progress-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
width: 0%;
|
||||
height: 10px;
|
||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||
border-radius: 5px;
|
||||
transition: width 0.1s ease;
|
||||
}
|
||||
|
||||
#progress-text {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
<div id="app">
|
||||
<canvas id="renderDom"></canvas>
|
||||
<div id="progress-container" style="display: none;">
|
||||
<div id="progress-bar"></div>
|
||||
<div id="progress-text">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
}
|
||||
<script type="module">
|
||||
// import { kernel } from './src/main.ts';
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
||||
|
||||
#renderDom {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<canvas id="renderDom"></canvas>
|
||||
// const config = {
|
||||
// container: document.querySelector('#renderDom'),
|
||||
// modelUrlList: ['/assets/model.glb'],
|
||||
// env: { envPath: '/assets/hdr.env', intensity: 1.2, rotationY: 0.3, background: false },
|
||||
// };
|
||||
|
||||
<!-- 模块化:Dev 使用 /src/main.ts,构建后改为 /assets/index.js -->
|
||||
<script type="module">
|
||||
const config = {
|
||||
container: document.querySelector('#renderDom'),
|
||||
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
|
||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
|
||||
};
|
||||
|
||||
import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
||||
const config = {
|
||||
container: 'renderDom',
|
||||
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
|
||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
|
||||
};
|
||||
|
||||
kernel.init(config);
|
||||
|
||||
|
||||
kernel.on('model:load:progress', (data) => {
|
||||
console.log('模型加载事件', data);
|
||||
});
|
||||
kernel.init(config);
|
||||
|
||||
|
||||
|
||||
kernel.on('model:loaded', (data) => {
|
||||
console.log('模型加载完成', data);
|
||||
});
|
||||
kernel.on('model:load:progress', (data) => {
|
||||
console.log('模型加载事件', data);
|
||||
|
||||
|
||||
|
||||
kernel.on('model:click', (data) => {
|
||||
console.log('模型点击事件', data);
|
||||
});
|
||||
const progress = data.progress || 0;
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = 'block';
|
||||
}
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${progress * 100}%`;
|
||||
}
|
||||
if (progressText) {
|
||||
progressText.textContent = `${Math.round(progress * 100)}%`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
kernel.on('model:loaded', (data) => {
|
||||
console.log('模型加载完成', data);
|
||||
// 隐藏进度条
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
kernel.on('all:ready', (data) => {
|
||||
console.log('所有模块加载完,', data);
|
||||
kernel.material.apply({
|
||||
target: 'Material__2',
|
||||
attribute: 'alpha',
|
||||
value: 0.5,
|
||||
});
|
||||
|
||||
|
||||
kernel.hotspot.render([
|
||||
{
|
||||
id: "h1",
|
||||
name: "卷帘门",
|
||||
meshName: "Valve_01",
|
||||
icon: "./btn_热点.png",
|
||||
offset: [25, 25, 0],
|
||||
radius: 20,
|
||||
color: "#21c7ff",
|
||||
payload: { type: "valve", code: "A" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
kernel.on('model:click', (data) => {
|
||||
console.log('模型点击事件', data);
|
||||
console.log(data);
|
||||
|
||||
});
|
||||
|
||||
kernel.on('hotspot:click', (data) => {
|
||||
console.log('热点被点击:', data);
|
||||
const { id, name } = data
|
||||
if (name === "卷帘门") {
|
||||
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
||||
|
||||
// Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
||||
const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
||||
console.log('设置剖切:', clipHeight);
|
||||
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 添加模型到场景
|
||||
await kernel.model.add('/models/car.glb');
|
||||
|
||||
// 销毁模型
|
||||
kernel.model.destroy('car');
|
||||
|
||||
// 替换模型
|
||||
await kernel.model.replace('car', '/models/new-car.glb');
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
421
index.html
421
index.html
@ -21,6 +21,12 @@
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#canvas-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@ -30,6 +36,157 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
#config-panel {
|
||||
width: 320px;
|
||||
background: rgba(30, 30, 45, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#config-panel::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
#config-panel::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
#config-panel::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.config-category {
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.category-header {
|
||||
padding: 12px 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background 0.3s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.category-header:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.category-header.active {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-arrow {
|
||||
transition: transform 0.3s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.category-arrow.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.category-content {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
overflow: hidden;
|
||||
transition: grid-template-rows 0.3s ease, padding 0.3s ease;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.category-content.expanded {
|
||||
grid-template-rows: 1fr;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.category-content>* {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.option-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.option-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.option-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.option-btn.selected {
|
||||
background: rgba(76, 175, 80, 0.6);
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.option-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.option-checkbox:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.option-checkbox input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option-checkbox label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
#progress-container {
|
||||
position: absolute;
|
||||
@ -63,108 +220,210 @@
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<canvas id="renderDom"></canvas>
|
||||
<div id="progress-container" style="display: none;">
|
||||
<div id="progress-bar"></div>
|
||||
<div id="progress-text">0%</div>
|
||||
<!-- 画布区域 -->
|
||||
<div id="canvas-container">
|
||||
<canvas id="renderDom"></canvas>
|
||||
<div id="progress-container" style="display: none;">
|
||||
<div id="progress-bar"></div>
|
||||
<div id="progress-text">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置面板 -->
|
||||
<div id="config-panel">
|
||||
<div class="config-title">选装选配</div>
|
||||
|
||||
<!-- 棚子尺寸 -->
|
||||
<div class="config-category">
|
||||
<div class="category-header" data-category="size">
|
||||
<span class="category-title">棚子尺寸</span>
|
||||
<span class="category-arrow">▼</span>
|
||||
</div>
|
||||
<div class="category-content">
|
||||
<div class="option-group">
|
||||
<button class="option-btn" data-option="size-1">3x3米</button>
|
||||
<button class="option-btn" data-option="size-2">4x4米</button>
|
||||
<button class="option-btn" data-option="size-3">5x5米</button>
|
||||
<button class="option-btn" data-option="size-4">6x6米</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 棚子类型 -->
|
||||
<div class="config-category">
|
||||
<div class="category-header" data-category="type">
|
||||
<span class="category-title">棚子类型</span>
|
||||
<span class="category-arrow">▼</span>
|
||||
</div>
|
||||
<div class="category-content">
|
||||
<div class="option-group">
|
||||
<button class="option-btn" data-option="type-1">平顶</button>
|
||||
<button class="option-btn" data-option="type-2">尖顶</button>
|
||||
<button class="option-btn" data-option="type-3">弧形</button>
|
||||
<button class="option-btn" data-option="type-4">异形</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 百叶 (多选) -->
|
||||
<div class="config-category">
|
||||
<div class="category-header" data-category="louver">
|
||||
<span class="category-title">百叶</span>
|
||||
<span class="category-arrow">▼</span>
|
||||
</div>
|
||||
<div class="category-content">
|
||||
<div class="option-group">
|
||||
<div class="option-checkbox">
|
||||
<input type="checkbox" id="louver-1" data-option="louver-1">
|
||||
<label for="louver-1">百叶A</label>
|
||||
</div>
|
||||
<div class="option-checkbox">
|
||||
<input type="checkbox" id="louver-2" data-option="louver-2">
|
||||
<label for="louver-2">百叶B</label>
|
||||
</div>
|
||||
<div class="option-checkbox">
|
||||
<input type="checkbox" id="louver-3" data-option="louver-3">
|
||||
<label for="louver-3">百叶C</label>
|
||||
</div>
|
||||
<div class="option-checkbox">
|
||||
<input type="checkbox" id="louver-4" data-option="louver-4">
|
||||
<label for="louver-4">百叶D</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配色 -->
|
||||
<div class="config-category">
|
||||
<div class="category-header" data-category="color">
|
||||
<span class="category-title">配色</span>
|
||||
<span class="category-arrow">▼</span>
|
||||
</div>
|
||||
<div class="category-content">
|
||||
<div class="option-group">
|
||||
<button class="option-btn" data-option="color-1">白色</button>
|
||||
<button class="option-btn" data-option="color-2">灰色</button>
|
||||
<button class="option-btn" data-option="color-3">黑色</button>
|
||||
<button class="option-btn" data-option="color-4">木色</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./index.js"></script>
|
||||
<script type="module">
|
||||
import { kernel } from './src/main.ts';
|
||||
|
||||
// import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
||||
// ========== UI 交互逻辑 ==========
|
||||
|
||||
const config = {
|
||||
container: document.querySelector('#renderDom'),
|
||||
modelUrlList: ['/assets/model.glb'],
|
||||
env: { envPath: '/assets/hdr.env', intensity: 1.2, rotationY: 0.3, background: false },
|
||||
};
|
||||
// 折叠面板切换
|
||||
document.querySelectorAll('.category-header').forEach(header => {
|
||||
header.addEventListener('click', function () {
|
||||
const content = this.nextElementSibling;
|
||||
const arrow = this.querySelector('.category-arrow');
|
||||
|
||||
kernel.init(config);
|
||||
|
||||
|
||||
|
||||
kernel.on('model:load:progress', (data) => {
|
||||
console.log('模型加载事件', data);
|
||||
|
||||
|
||||
|
||||
const progress = data.progress || 0;
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = 'block';
|
||||
}
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${progress * 100}%`;
|
||||
}
|
||||
if (progressText) {
|
||||
progressText.textContent = `${Math.round(progress * 100)}%`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
kernel.on('model:loaded', (data) => {
|
||||
console.log('模型加载完成', data);
|
||||
// 隐藏进度条
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
kernel.on('all:ready', (data) => {
|
||||
console.log('所有模块加载完,', data);
|
||||
kernel.material.apply({
|
||||
target: 'Material__2',
|
||||
attribute: 'alpha',
|
||||
value: 0.5,
|
||||
// 切换展开/收起状态
|
||||
content.classList.toggle('expanded');
|
||||
arrow.classList.toggle('expanded');
|
||||
this.classList.toggle('active');
|
||||
});
|
||||
|
||||
kernel.hotspot.render([
|
||||
{
|
||||
id: "h1",
|
||||
name: "卷帘门",
|
||||
meshName: "Valve_01",
|
||||
icon: "/assets/btn_热点.png",
|
||||
offset: [25, 25, 0],
|
||||
radius: 20,
|
||||
color: "#21c7ff",
|
||||
payload: { type: "valve", code: "A" },
|
||||
},
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
// 单选按钮逻辑
|
||||
document.querySelectorAll('.option-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const optionGroup = this.parentElement;
|
||||
const category = this.closest('.config-category');
|
||||
const categoryName = category.querySelector('.category-header').dataset.category;
|
||||
|
||||
kernel.on('model:click', (data) => {
|
||||
console.log('模型点击事件', data);
|
||||
console.log(data);
|
||||
// 同一组内取消其他选中状态
|
||||
optionGroup.querySelectorAll('.option-btn').forEach(b => {
|
||||
b.classList.remove('selected');
|
||||
});
|
||||
|
||||
// 切换卷帘门开关
|
||||
// 选中当前按钮
|
||||
this.classList.add('selected');
|
||||
|
||||
// 触发自定义事件
|
||||
const event = new CustomEvent('config:change', {
|
||||
detail: {
|
||||
category: categoryName,
|
||||
value: this.dataset.option,
|
||||
text: this.textContent
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
console.log('配置变更:', {
|
||||
category: categoryName,
|
||||
value: this.dataset.option,
|
||||
text: this.textContent
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
kernel.on('hotspot:click', (data) => {
|
||||
console.log('热点被点击:', data);
|
||||
const { id, name } = data
|
||||
if (name === "卷帘门") {
|
||||
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
||||
// 多选复选框逻辑
|
||||
document.querySelectorAll('.option-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
const category = this.closest('.config-category');
|
||||
const categoryName = category.querySelector('.category-header').dataset.category;
|
||||
const optionGroup = this.closest('.option-group');
|
||||
|
||||
// Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
||||
const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
||||
console.log('设置剖切:', clipHeight);
|
||||
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
||||
}
|
||||
// 获取当前组所有选中的值
|
||||
const selectedValues = Array.from(
|
||||
optionGroup.querySelectorAll('input[type="checkbox"]:checked')
|
||||
).map(cb => ({
|
||||
value: cb.dataset.option,
|
||||
text: cb.nextElementSibling.textContent
|
||||
}));
|
||||
|
||||
// data 包含: { id, name, meshName, payload }
|
||||
// 触发自定义事件
|
||||
const event = new CustomEvent('config:change', {
|
||||
detail: {
|
||||
category: categoryName,
|
||||
values: selectedValues,
|
||||
checked: this.checked,
|
||||
currentValue: this.dataset.option
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
console.log('配置变更(多选):', {
|
||||
category: categoryName,
|
||||
selectedValues: selectedValues,
|
||||
checked: this.checked,
|
||||
currentValue: this.dataset.option
|
||||
});
|
||||
if (category === "louver") {
|
||||
selectedValues.forEach(element => {
|
||||
if (checked) {
|
||||
kernel.model.add(element.text,`https://sdk.zguiy.com/resurces/model/${element.text}.glb`);
|
||||
} else {
|
||||
kernel.model.destroy(element.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// kernel.model.add('https://sdk.zguiy.com/resurces/model/百叶1.glb');
|
||||
});
|
||||
});
|
||||
|
||||
// 监听配置变更事件(供外部使用)
|
||||
document.addEventListener('config:change', function (e) {
|
||||
// 这里可以根据配置变更来操作 3D 模型
|
||||
// 例如:
|
||||
// if (e.detail.category === 'size') {
|
||||
// kernel.model.replace('shed', `/models/shed-${e.detail.value}.glb`);
|
||||
// }
|
||||
// if (e.detail.category === 'color') {
|
||||
// kernel.material.apply({
|
||||
// target: 'ShedMaterial',
|
||||
// attribute: 'baseColor',
|
||||
// value: getColorValue(e.detail.value)
|
||||
// });
|
||||
// }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
108
index.js
Normal file
108
index.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { kernel } from './src/main.ts';
|
||||
|
||||
// import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
||||
|
||||
// const config = {
|
||||
// container: document.querySelector('#renderDom'),
|
||||
// modelUrlList: ['/assets/model.glb'],
|
||||
// env: { envPath: '/assets/hdr.env', intensity: 1.2, rotationY: 0.3, background: false },
|
||||
// };
|
||||
|
||||
const config = {
|
||||
container: document.querySelector('#renderDom'),
|
||||
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
|
||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
|
||||
};
|
||||
|
||||
kernel.init(config);
|
||||
|
||||
|
||||
|
||||
kernel.on('model:load:progress', (data) => {
|
||||
console.log('模型加载事件', data);
|
||||
|
||||
|
||||
|
||||
const progress = data.progress || 0;
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = 'block';
|
||||
}
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${progress * 100}%`;
|
||||
}
|
||||
if (progressText) {
|
||||
progressText.textContent = `${Math.round(progress * 100)}%`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
kernel.on('model:loaded', (data) => {
|
||||
console.log('模型加载完成', data);
|
||||
// 隐藏进度条
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
kernel.on('all:ready', (data) => {
|
||||
console.log('所有模块加载完,', data);
|
||||
kernel.material.apply({
|
||||
target: 'Material__2',
|
||||
attribute: 'alpha',
|
||||
value: 0.5,
|
||||
});
|
||||
|
||||
|
||||
kernel.hotspot.render([
|
||||
{
|
||||
id: "h1",
|
||||
name: "卷帘门",
|
||||
meshName: "Valve_01",
|
||||
icon: "https://bpic.588ku.com/element_pic/20/06/30/d1046b01afc0b9586844350d131f4daf.jpg!/fw/253/quality/90/unsharp/true/compress/true",
|
||||
offset: [25, 25, 0],
|
||||
radius: 20,
|
||||
color: "#21c7ff",
|
||||
payload: { type: "valve", code: "A" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
kernel.on('model:click', (data) => {
|
||||
console.log('模型点击事件', data);
|
||||
console.log(data);
|
||||
|
||||
});
|
||||
|
||||
kernel.on('hotspot:click', (data) => {
|
||||
console.log('热点被点击:', data);
|
||||
const { id, name } = data
|
||||
if (name === "卷帘门") {
|
||||
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
||||
|
||||
// Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
||||
const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
||||
console.log('设置剖切:', clipHeight);
|
||||
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 添加模型到场景
|
||||
// await kernel.model.add('https://sdk.zguiy.com/resurces/model/百叶1.glb');
|
||||
|
||||
// 销毁模型
|
||||
// kernel.model.destroy('car');
|
||||
|
||||
// 替换模型
|
||||
// await kernel.model.replace('car', '/models/new-car.glb');
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
/**
|
||||
* 模拟 ARKit 数据的 WebSocket 服务器
|
||||
* 运行: node mock-arkit-server.js
|
||||
*/
|
||||
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const PORT = 8765;
|
||||
const wss = new WebSocket.Server({ port: PORT });
|
||||
|
||||
// ARKit 52 个 blendShape 完整列表
|
||||
const ARKIT_BLENDSHAPES = [
|
||||
'eyeBlinkLeft', 'eyeLookDownLeft', 'eyeLookInLeft', 'eyeLookOutLeft', 'eyeLookUpLeft', 'eyeSquintLeft', 'eyeWideLeft',
|
||||
'eyeBlinkRight', 'eyeLookDownRight', 'eyeLookInRight', 'eyeLookOutRight', 'eyeLookUpRight', 'eyeSquintRight', 'eyeWideRight',
|
||||
'jawForward', 'jawLeft', 'jawRight', 'jawOpen',
|
||||
'mouthClose', 'mouthFunnel', 'mouthPucker', 'mouthLeft', 'mouthRight',
|
||||
'mouthSmileLeft', 'mouthSmileRight', 'mouthFrownLeft', 'mouthFrownRight',
|
||||
'mouthDimpleLeft', 'mouthDimpleRight', 'mouthStretchLeft', 'mouthStretchRight',
|
||||
'mouthRollLower', 'mouthRollUpper', 'mouthShrugLower', 'mouthShrugUpper',
|
||||
'mouthPressLeft', 'mouthPressRight', 'mouthLowerDownLeft', 'mouthLowerDownRight',
|
||||
'mouthUpperUpLeft', 'mouthUpperUpRight',
|
||||
'browDownLeft', 'browDownRight', 'browInnerUp', 'browOuterUpLeft', 'browOuterUpRight',
|
||||
'cheekPuff', 'cheekSquintLeft', 'cheekSquintRight',
|
||||
'noseSneerLeft', 'noseSneerRight', 'tongueOut'
|
||||
];
|
||||
|
||||
// 表情预设
|
||||
const expressions = [
|
||||
{ name: '微笑', data: { mouthSmileLeft: 0.8, mouthSmileRight: 0.8, cheekSquintLeft: 0.3, cheekSquintRight: 0.3 } },
|
||||
{ name: '张嘴说话', data: { jawOpen: 0.5, mouthFunnel: 0.3 } },
|
||||
{ name: '惊讶', data: { eyeWideLeft: 0.9, eyeWideRight: 0.9, jawOpen: 0.6, browInnerUp: 0.7 } },
|
||||
{ name: '皱眉', data: { browDownLeft: 0.8, browDownRight: 0.8, eyeSquintLeft: 0.3, eyeSquintRight: 0.3 } },
|
||||
{ name: '嘟嘴', data: { mouthPucker: 0.9, mouthFunnel: 0.4 } },
|
||||
{ name: '吐舌', data: { tongueOut: 0.7, jawOpen: 0.4 } },
|
||||
{ name: '生气', data: { noseSneerLeft: 0.7, noseSneerRight: 0.7, browDownLeft: 0.6, browDownRight: 0.6, jawOpen: 0.2 } },
|
||||
{ name: '悲伤', data: { mouthFrownLeft: 0.7, mouthFrownRight: 0.7, browInnerUp: 0.5, eyeSquintLeft: 0.2, eyeSquintRight: 0.2 } },
|
||||
{ name: '中性', data: {} },
|
||||
];
|
||||
|
||||
let currentExprIndex = 0;
|
||||
let transitionProgress = 0;
|
||||
let blinkTimer = 0;
|
||||
let isBlinking = false;
|
||||
|
||||
function lerp(a, b, t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
function generateBlendShapes() {
|
||||
const current = expressions[currentExprIndex].data;
|
||||
const next = expressions[(currentExprIndex + 1) % expressions.length].data;
|
||||
|
||||
// 初始化所有 52 个 blendShape 为 0
|
||||
const blendShapes = {};
|
||||
ARKIT_BLENDSHAPES.forEach(name => blendShapes[name] = 0);
|
||||
|
||||
// 插值当前和下一个表情
|
||||
for (const key of ARKIT_BLENDSHAPES) {
|
||||
const currentVal = current[key] || 0;
|
||||
const nextVal = next[key] || 0;
|
||||
blendShapes[key] = lerp(currentVal, nextVal, transitionProgress);
|
||||
}
|
||||
|
||||
// 自然眨眼(每3-5秒眨一次)
|
||||
blinkTimer++;
|
||||
if (!isBlinking && blinkTimer > 90 + Math.random() * 60) {
|
||||
isBlinking = true;
|
||||
blinkTimer = 0;
|
||||
}
|
||||
if (isBlinking) {
|
||||
const blinkProgress = blinkTimer / 6;
|
||||
if (blinkProgress < 1) {
|
||||
blendShapes.eyeBlinkLeft = Math.sin(blinkProgress * Math.PI);
|
||||
blendShapes.eyeBlinkRight = Math.sin(blinkProgress * Math.PI);
|
||||
} else {
|
||||
isBlinking = false;
|
||||
blinkTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加微小的随机抖动(更自然)
|
||||
blendShapes.jawOpen += (Math.random() - 0.5) * 0.02;
|
||||
blendShapes.browInnerUp += (Math.random() - 0.5) * 0.01;
|
||||
|
||||
// 表情过渡
|
||||
transitionProgress += 0.015;
|
||||
if (transitionProgress >= 1) {
|
||||
transitionProgress = 0;
|
||||
currentExprIndex = (currentExprIndex + 1) % expressions.length;
|
||||
console.log(`切换到表情: ${expressions[currentExprIndex].name}`);
|
||||
}
|
||||
|
||||
return blendShapes;
|
||||
}
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('客户端已连接');
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
const data = generateBlendShapes();
|
||||
ws.send(JSON.stringify(data));
|
||||
}
|
||||
}, 33); // ~30fps
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('客户端断开');
|
||||
clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`ARKit 模拟服务器运行在 ws://localhost:${PORT}`);
|
||||
console.log('表情循环: ' + expressions.map(e => e.name).join(' -> '));
|
||||
@ -148,9 +148,9 @@ export class AppHotspot extends Monobehiver {
|
||||
}
|
||||
|
||||
// 释放sprite资源
|
||||
if (point.sprite) {
|
||||
point.sprite.dispose();
|
||||
}
|
||||
// if (point.sprite) {
|
||||
// point.sprite.dispose();
|
||||
// }
|
||||
}
|
||||
|
||||
// 清空热点池
|
||||
|
||||
@ -149,6 +149,54 @@ export class AppModel extends Monobehiver {
|
||||
this.skeletonMerged = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加模型到场景
|
||||
* @param modelUrl 模型URL路径
|
||||
*/
|
||||
async addModel(modelUrl: string): Promise<LoadResult> {
|
||||
const handleProgress = (event: ISceneLoaderProgressEvent): void => {
|
||||
const progress = event.lengthComputable && event.total > 0
|
||||
? Math.min(1, event.loaded / event.total)
|
||||
: 0;
|
||||
EventBridge.modelLoadProgress({
|
||||
loaded: progress,
|
||||
total: 1,
|
||||
url: modelUrl,
|
||||
progress,
|
||||
percentage: Number((progress * 100).toFixed(2)),
|
||||
detail: {
|
||||
url: modelUrl,
|
||||
lengthComputable: event.lengthComputable,
|
||||
loadedBytes: event.loaded,
|
||||
totalBytes: event.total
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const result = await this.loadSingleModel(modelUrl, handleProgress);
|
||||
|
||||
if (result.success) {
|
||||
EventBridge.modelLoaded({ urls: [modelUrl] });
|
||||
} else {
|
||||
EventBridge.modelLoadError({ url: modelUrl, error: result.error });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换模型:销毁旧模型并加载新模型
|
||||
* @param modelName 要替换的模型名称
|
||||
* @param newModelUrl 新模型的URL路径
|
||||
*/
|
||||
async replaceModel(modelName: string, newModelUrl: string): Promise<LoadResult> {
|
||||
// 先销毁旧模型
|
||||
this.destroyModel(modelName);
|
||||
|
||||
// 加载新模型
|
||||
return await this.addModel(newModelUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁指定模型
|
||||
* @param modelName 模型名称
|
||||
|
||||
@ -59,7 +59,7 @@ export class EventBridge {
|
||||
static onSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
|
||||
return on("scene:ready", callback, context);
|
||||
}
|
||||
static onAllReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
|
||||
static onAllReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
|
||||
return on("all:ready", callback, context);
|
||||
}
|
||||
static onHotspotClick(callback: (payload: HotspotClickPayload) => void, context?: unknown): Emitter {
|
||||
|
||||
@ -48,12 +48,12 @@ class HotSpot {
|
||||
Point_Event(prams: HotspotPrams) {
|
||||
const iconPath = prams.icon
|
||||
|
||||
|
||||
// 为每个热点创建独立的材质
|
||||
const texture = new Texture(iconPath, this.mainApp.appScene.object)
|
||||
texture.hasAlpha = true
|
||||
texture.getAlphaFromRGB = false
|
||||
|
||||
|
||||
const material = new StandardMaterial(`hotspotMaterial_${Math.random()}`, this.mainApp.appScene.object)
|
||||
material.diffuseTexture = texture
|
||||
material.emissiveTexture = texture
|
||||
@ -64,15 +64,10 @@ class HotSpot {
|
||||
material.backFaceCulling = false
|
||||
|
||||
// 检查纹理是否已加载
|
||||
if (texture.isReady()) {
|
||||
// 纹理已准备好,立即创建热点
|
||||
this.createPointPlane(prams, material)
|
||||
} else {
|
||||
// 纹理未准备好,等待加载完成
|
||||
texture.onLoadObservable.addOnce(() => {
|
||||
|
||||
this.createPointPlane(prams, material)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 创建点平面的具体实现
|
||||
|
||||
@ -14,10 +14,26 @@ export class KernelAdapter {
|
||||
/** 模型管理 */
|
||||
model = {
|
||||
/**
|
||||
* 销毁指定模<EFBFBD>? * @param modelName 模型名称
|
||||
* 添加模型到场景
|
||||
* @param modelUrl 模型URL路径
|
||||
*/
|
||||
add: async (modelUrl: string): Promise<void> => {
|
||||
await this.mainApp.appModel.addModel(modelUrl);
|
||||
},
|
||||
/**
|
||||
* 销毁指定模型
|
||||
* @param modelName 模型名称
|
||||
*/
|
||||
destroy: (modelName: string): void => {
|
||||
this.mainApp.appModel.destroyModel(modelName);
|
||||
},
|
||||
/**
|
||||
* 替换模型
|
||||
* @param modelName 要替换的模型名称
|
||||
* @param newModelUrl 新模型的URL路径
|
||||
*/
|
||||
replace: async (modelName: string, newModelUrl: string): Promise<void> => {
|
||||
await this.mainApp.appModel.replaceModel(modelName, newModelUrl);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
173
test/customization-3d-copy.js
Normal file
173
test/customization-3d-copy.js
Normal file
@ -0,0 +1,173 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!document.querySelector('.customization-3d-wrapper')) return;
|
||||
|
||||
const get3DViewer = () => window.Customization3DViewer;
|
||||
|
||||
const load3DModel = (modelUrl, productId) => {
|
||||
const viewer = get3DViewer();
|
||||
return viewer ? viewer.loadModel(modelUrl, productId) : Promise.resolve(false);
|
||||
};
|
||||
|
||||
const clear3DModel = () => {
|
||||
const viewer = get3DViewer();
|
||||
return viewer ? viewer.clearModel() : Promise.resolve();
|
||||
};
|
||||
|
||||
const show3DEmpty = () => {
|
||||
get3DViewer()?.showEmpty();
|
||||
};
|
||||
|
||||
const get3DModelUrl = (productId, variantId, wrapper) => {
|
||||
const viewer = get3DViewer();
|
||||
return viewer ? viewer.getModelUrl(productId, variantId, wrapper) : Promise.resolve(null);
|
||||
};
|
||||
|
||||
const get3DHotspots = () => {
|
||||
const hotspots = window.CUSTOMIZATION_3D_HOTSPOTS;
|
||||
if (!Array.isArray(hotspots)) return [];
|
||||
return hotspots
|
||||
.filter((item) => item && typeof item === 'object')
|
||||
.map((item) => {
|
||||
const meshName = String(item.meshName || '').trim();
|
||||
if (!meshName) return null;
|
||||
|
||||
let offset = [0, 0, 0];
|
||||
if (Array.isArray(item.offset) && item.offset.length >= 3) {
|
||||
const parsed = item.offset.slice(0, 3).map((v) => Number(v));
|
||||
if (parsed.every(Number.isFinite)) {
|
||||
const maxAbs = Math.max(...parsed.map((v) => Math.abs(v)));
|
||||
offset = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
const next = {
|
||||
id: String(item.id || meshName),
|
||||
name: String(item.name || item.id || meshName),
|
||||
meshName,
|
||||
offset,
|
||||
};
|
||||
|
||||
const color = String(item.color || '').trim();
|
||||
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)) next.color = color;
|
||||
|
||||
const r = Number(item.radius);
|
||||
if (Number.isFinite(r) && r > 0) {
|
||||
next.radius = Math.min(Math.max(r, 0.5), 30);
|
||||
} else {
|
||||
const defaultRadius = Number(window.CUSTOMIZATION_3D_HOTSPOT_RADIUS_DEFAULT);
|
||||
next.radius = Number.isFinite(defaultRadius) && defaultRadius > 0
|
||||
? Math.min(30, defaultRadius)
|
||||
: 18;
|
||||
}
|
||||
|
||||
const icon = String(item.icon || '').trim();
|
||||
if (icon && (/^https?:\/\//i.test(icon) || icon.startsWith('//'))) {
|
||||
next.icon = icon;
|
||||
}
|
||||
|
||||
if (item.payload && typeof item.payload === 'object' && !Array.isArray(item.payload)) {
|
||||
next.payload = item.payload;
|
||||
}
|
||||
|
||||
return next;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const getHotspotActionConfig = (detail = {}) => {
|
||||
const all = window.CUSTOMIZATION_3D_HOTSPOT_ACTIONS;
|
||||
if (!all || typeof all !== 'object') return null;
|
||||
return all[detail.id] || all[detail.name] || null;
|
||||
};
|
||||
|
||||
const handle3DHotspotClick = (event) => {
|
||||
const detail = event?.detail || {};
|
||||
const viewer = get3DViewer();
|
||||
if (!viewer) return;
|
||||
|
||||
if (typeof window.CUSTOMIZATION_3D_ON_HOTSPOT_CLICK === 'function') {
|
||||
try {
|
||||
window.CUSTOMIZATION_3D_ON_HOTSPOT_CLICK(detail, viewer);
|
||||
} catch (err) {
|
||||
console.warn('[Customization] CUSTOMIZATION_3D_ON_HOTSPOT_CLICK failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const action = getHotspotActionConfig(detail);
|
||||
if (action?.door) {
|
||||
viewer.door?.toggle(action.door);
|
||||
}
|
||||
if (action?.clipping) {
|
||||
const clip = action.clipping;
|
||||
if (typeof clip.height === 'number') {
|
||||
viewer.clipping?.setY(
|
||||
clip.height,
|
||||
clip.keepBelow !== false,
|
||||
Array.isArray(clip.meshNames) ? clip.meshNames : []
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setup3DEventBridge = () => {
|
||||
if (document.documentElement.dataset.customization3dEventsBoundCopy === '1') return;
|
||||
document.documentElement.dataset.customization3dEventsBoundCopy = '1';
|
||||
|
||||
let hotspotRenderTimer = null;
|
||||
const renderConfiguredHotspots = () => {
|
||||
if (hotspotRenderTimer) clearTimeout(hotspotRenderTimer);
|
||||
hotspotRenderTimer = setTimeout(() => {
|
||||
hotspotRenderTimer = null;
|
||||
const hotspots = get3DHotspots();
|
||||
const viewer = get3DViewer();
|
||||
viewer?.hotspot?.clear?.();
|
||||
if (!hotspots.length) return;
|
||||
viewer?.hotspot?.render(hotspots);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
document.addEventListener('3d:scene:ready', renderConfiguredHotspots);
|
||||
document.addEventListener('3d:hotspots:update', renderConfiguredHotspots);
|
||||
document.addEventListener('3d:hotspot:click', handle3DHotspotClick);
|
||||
};
|
||||
|
||||
const setupWheelScrollLockOn3DContainer = () => {
|
||||
const container = document.querySelector('[data-3d-container]');
|
||||
if (!container || container.dataset.customization3dWheelLockCopy === '1') return;
|
||||
container.dataset.customization3dWheelLockCopy = '1';
|
||||
container.addEventListener(
|
||||
'wheel',
|
||||
(e) => {
|
||||
if (!container.contains(e.target)) return;
|
||||
e.preventDefault();
|
||||
},
|
||||
{ capture: true, passive: false }
|
||||
);
|
||||
};
|
||||
|
||||
const refreshHotspots = () => {
|
||||
document.dispatchEvent(new CustomEvent('3d:hotspots:update', { bubbles: true }));
|
||||
};
|
||||
|
||||
window.Customization3DInteractions = {
|
||||
load3DModel,
|
||||
clear3DModel,
|
||||
show3DEmpty,
|
||||
get3DModelUrl,
|
||||
get3DHotspots,
|
||||
refreshHotspots,
|
||||
};
|
||||
|
||||
const init3DInteractions = () => {
|
||||
setup3DEventBridge();
|
||||
setupWheelScrollLockOn3DContainer();
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init3DInteractions);
|
||||
} else {
|
||||
init3DInteractions();
|
||||
}
|
||||
})();
|
||||
455
test/customization-3d-viewer.js
Normal file
455
test/customization-3d-viewer.js
Normal file
@ -0,0 +1,455 @@
|
||||
window.Customization3DViewer = (function () {
|
||||
'use strict';
|
||||
|
||||
const FALLBACK_ENV_URL = 'https://sdk.zguiy.com/resurces/hdr/hdr.env';
|
||||
const CANVAS_ID = 'preview-3d-viewer';
|
||||
const RUNTIME_MODEL_ID = 'main-model';
|
||||
|
||||
let kernel = null;
|
||||
let sdkInitialized = false;
|
||||
let initPromise = null;
|
||||
let sdkCapabilityLogged = false;
|
||||
let sdkEventsBound = false;
|
||||
|
||||
let currentModelId = null;
|
||||
let currentModelUrl = null;
|
||||
|
||||
const hasFn = (obj, key) => !!obj && typeof obj[key] === 'function';
|
||||
const awaitIfPromise = (v) => (v?.then ? v : Promise.resolve(v));
|
||||
|
||||
const el = {
|
||||
loading: () => document.querySelector('[data-3d-loading]'),
|
||||
empty: () => document.querySelector('[data-3d-empty]'),
|
||||
container: () => document.querySelector('[data-3d-container]'),
|
||||
progressBar: () => document.querySelector('[data-3d-progress-bar]'),
|
||||
progressText: () => document.querySelector('[data-3d-progress-text]'),
|
||||
};
|
||||
|
||||
const ensureCanvas = () => {
|
||||
const existing = document.getElementById(CANVAS_ID);
|
||||
if (!existing) return null;
|
||||
if (existing.tagName === 'CANVAS') return existing;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = CANVAS_ID;
|
||||
canvas.className = existing.className || 'preview-3d-viewer';
|
||||
canvas.style.cssText = 'width:100%;height:100%;display:block;';
|
||||
existing.innerHTML = '';
|
||||
existing.appendChild(canvas);
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const resizeCanvas = (canvas) => {
|
||||
if (!canvas) return;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const { width, height } = canvas.getBoundingClientRect();
|
||||
const w = Math.max(1, Math.round(width * dpr));
|
||||
const h = Math.max(1, Math.round(height * dpr));
|
||||
if (canvas.width !== w) canvas.width = w;
|
||||
if (canvas.height !== h) canvas.height = h;
|
||||
};
|
||||
|
||||
const poll = (check, maxWait = 5000, interval = 100) =>
|
||||
new Promise((resolve) => {
|
||||
if (check()) { resolve(true); return; }
|
||||
const start = Date.now();
|
||||
const ticker = setInterval(() => {
|
||||
if (check()) {
|
||||
clearInterval(ticker);
|
||||
resolve(true);
|
||||
} else if (Date.now() - start >= maxWait) {
|
||||
clearInterval(ticker);
|
||||
resolve(false);
|
||||
}
|
||||
}, interval);
|
||||
});
|
||||
|
||||
const waitForSDK = () => poll(() => !!window.faceSDK?.kernel);
|
||||
const waitForContainer = () => {
|
||||
const canvas = ensureCanvas();
|
||||
if (!canvas) return Promise.resolve(false);
|
||||
return poll(() => {
|
||||
const { width, height } = canvas.getBoundingClientRect();
|
||||
return width > 0 && height > 0 && canvas.offsetParent !== null;
|
||||
}).then((ready) => {
|
||||
if (ready) resizeCanvas(canvas);
|
||||
return ready;
|
||||
});
|
||||
};
|
||||
|
||||
const showLoading = (progress = 0) => {
|
||||
const pct = Math.round(progress);
|
||||
if (el.loading()) el.loading().style.display = 'flex';
|
||||
if (el.empty()) el.empty().style.display = 'none';
|
||||
if (el.progressBar()) el.progressBar().style.width = `${pct}%`;
|
||||
if (el.progressText()) el.progressText().textContent = `${pct}%`;
|
||||
};
|
||||
|
||||
const showEmpty = () => {
|
||||
if (el.loading()) el.loading().style.display = 'none';
|
||||
if (el.empty()) el.empty().style.display = 'flex';
|
||||
if (el.container()) el.container().classList.remove('has-model');
|
||||
currentModelId = null;
|
||||
currentModelUrl = null;
|
||||
};
|
||||
|
||||
const showModelReady = () => {
|
||||
if (el.progressBar()) el.progressBar().style.width = '100%';
|
||||
if (el.progressText()) el.progressText().textContent = '100%';
|
||||
setTimeout(() => {
|
||||
if (el.loading()) el.loading().style.display = 'none';
|
||||
if (el.empty()) el.empty().style.display = 'none';
|
||||
if (el.container()) el.container().classList.add('has-model');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const getEnvUrl = () =>
|
||||
window.CUSTOMIZATION_3D_ENV_URL || FALLBACK_ENV_URL;
|
||||
|
||||
const buildInitConfig = (canvas, modelUrlList = []) => ({
|
||||
container: canvas,
|
||||
modelUrlList,
|
||||
env: {
|
||||
// envPath: getEnvUrl(),
|
||||
envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env',
|
||||
intensity: 1.2,
|
||||
rotationY: 0.3,
|
||||
background: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getSDKCapability = () => {
|
||||
const k = kernel || window.faceSDK?.kernel;
|
||||
return {
|
||||
'kernel.init': hasFn(k, 'init'),
|
||||
'kernel.on': hasFn(k, 'on'),
|
||||
'kernel.off': hasFn(k, 'off'),
|
||||
'camera.set': hasFn(k?.camera, 'set'),
|
||||
'camera.animateTo': hasFn(k?.camera, 'animateTo'),
|
||||
'lights.update': hasFn(k?.lights, 'update'),
|
||||
'environment.setHDRI': hasFn(k?.environment, 'setHDRI'),
|
||||
'hotspot.render': hasFn(k?.hotspot, 'render'),
|
||||
'hotspot.on': hasFn(k?.hotspot, 'on'),
|
||||
'model.load': hasFn(k?.model, 'load'),
|
||||
'model.replace': hasFn(k?.model, 'replace'),
|
||||
'model.destroy': hasFn(k?.model, 'destroy'),
|
||||
'model.on': hasFn(k?.model, 'on'),
|
||||
'material.apply': hasFn(k?.material, 'apply'),
|
||||
'material.batch': hasFn(k?.material, 'batch'),
|
||||
'material.reset': hasFn(k?.material, 'reset'),
|
||||
'debug': hasFn(k, 'debug'),
|
||||
};
|
||||
};
|
||||
|
||||
const useRuntimeModelAPI = () => {
|
||||
const cap = getSDKCapability();
|
||||
return cap['model.load'] && cap['model.replace'];
|
||||
};
|
||||
|
||||
const logCapabilityOnce = () => {
|
||||
if (sdkCapabilityLogged) return;
|
||||
sdkCapabilityLogged = true;
|
||||
const cap = getSDKCapability();
|
||||
const strategy = useRuntimeModelAPI()
|
||||
? 'runtime model API (load / replace / destroy)'
|
||||
: 'kernel.init(modelUrlList) fallback';
|
||||
console.groupCollapsed('[3D] faceSDK capability report');
|
||||
console.table(cap);
|
||||
console.log('[3D] active load strategy:', strategy);
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
const bindSDKEvents = () => {
|
||||
if (!hasFn(kernel, 'on') || sdkEventsBound) return;
|
||||
sdkEventsBound = true;
|
||||
|
||||
kernel.on('model:load:progress', ({ progress = 0 } = {}) => showLoading(progress));
|
||||
kernel.on('model:loaded', () => showModelReady());
|
||||
kernel.on('model:replaced', () => showModelReady());
|
||||
kernel.on('all:ready', (data) => {
|
||||
document.dispatchEvent(new CustomEvent('3d:scene:ready', { detail: data, bubbles: true }));
|
||||
});
|
||||
|
||||
kernel.on('model:click', (data) =>
|
||||
document.dispatchEvent(new CustomEvent('3d:model:click', { detail: data, bubbles: true }))
|
||||
);
|
||||
kernel.on('hotspot:click', (data) =>
|
||||
document.dispatchEvent(new CustomEvent('3d:hotspot:click', { detail: data, bubbles: true }))
|
||||
);
|
||||
|
||||
if (hasFn(kernel?.hotspot, 'on')) {
|
||||
kernel.hotspot.on('click', (data) =>
|
||||
document.dispatchEvent(new CustomEvent('3d:hotspot:click', { detail: data, bubbles: true }))
|
||||
);
|
||||
kernel.hotspot.on('hover', (data) =>
|
||||
document.dispatchEvent(new CustomEvent('3d:hotspot:hover', { detail: data, bubbles: true }))
|
||||
);
|
||||
kernel.hotspot.on('rendered', () =>
|
||||
document.dispatchEvent(new CustomEvent('3d:hotspot:rendered', { bubbles: true }))
|
||||
);
|
||||
}
|
||||
|
||||
if (hasFn(kernel, 'on')) {
|
||||
kernel.on('camera:changed', (state) =>
|
||||
document.dispatchEvent(new CustomEvent('3d:camera:changed', { detail: state, bubbles: true }))
|
||||
);
|
||||
}
|
||||
|
||||
kernel.on('env:error', (err) => console.warn('[3D] Environment map error:', err));
|
||||
};
|
||||
|
||||
const initSDK = async () => {
|
||||
if (sdkInitialized) return true;
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
const [sdkReady, containerReady] = await Promise.all([
|
||||
waitForSDK(),
|
||||
waitForContainer(),
|
||||
]);
|
||||
if (!sdkReady || !containerReady) {
|
||||
throw new Error('[3D] SDK or container not ready');
|
||||
}
|
||||
|
||||
const canvas = ensureCanvas();
|
||||
if (!canvas) throw new Error('[3D] Canvas element not found');
|
||||
|
||||
if (canvas.getBoundingClientRect().width === 0) {
|
||||
canvas.style.minWidth = '100px';
|
||||
canvas.style.minHeight = '100px';
|
||||
}
|
||||
resizeCanvas(canvas);
|
||||
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
|
||||
kernel = window.faceSDK.kernel;
|
||||
logCapabilityOnce();
|
||||
bindSDKEvents();
|
||||
|
||||
await awaitIfPromise(kernel.init(buildInitConfig(canvas, [])));
|
||||
|
||||
sdkInitialized = true;
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[3D] Initialization failed:', err);
|
||||
return false;
|
||||
} finally {
|
||||
initPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return initPromise;
|
||||
};
|
||||
|
||||
const loadModel = async (modelUrl, productId = null) => {
|
||||
if (!modelUrl) {
|
||||
showEmpty(); return false;
|
||||
}
|
||||
|
||||
if (!sdkInitialized && !await initSDK()) { showEmpty(); return false; }
|
||||
|
||||
const modelId = productId ? `product-${productId}` : RUNTIME_MODEL_ID;
|
||||
|
||||
if (currentModelId === modelId && currentModelUrl === modelUrl) return true;
|
||||
|
||||
try {
|
||||
showLoading(0);
|
||||
resizeCanvas(ensureCanvas());
|
||||
|
||||
if (useRuntimeModelAPI()) {
|
||||
if (currentModelUrl) {
|
||||
await awaitIfPromise(
|
||||
kernel.model.replace(RUNTIME_MODEL_ID, { url: modelUrl, draco: true })
|
||||
);
|
||||
} else {
|
||||
await awaitIfPromise(
|
||||
kernel.model.load({ id: RUNTIME_MODEL_ID, url: modelUrl, draco: true })
|
||||
);
|
||||
}
|
||||
} else {
|
||||
bindSDKEvents();
|
||||
await awaitIfPromise(
|
||||
kernel.init(buildInitConfig(ensureCanvas(), [modelUrl]))
|
||||
);
|
||||
}
|
||||
|
||||
currentModelId = modelId;
|
||||
currentModelUrl = modelUrl;
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[3D] loadModel failed:', err);
|
||||
showEmpty();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearModel = async () => {
|
||||
if (sdkInitialized && hasFn(kernel?.model, 'destroy')) {
|
||||
try {
|
||||
await awaitIfPromise(kernel.model.destroy(RUNTIME_MODEL_ID));
|
||||
} catch (err) {
|
||||
console.warn('[3D] clearModel: model destroy failed:', err);
|
||||
}
|
||||
}
|
||||
currentModelId = null;
|
||||
currentModelUrl = null;
|
||||
showEmpty();
|
||||
};
|
||||
|
||||
const getModelUrl = async (productId, _variantId = null, wrapper = null) => {
|
||||
const card = wrapper
|
||||
|| document.querySelector(`.product-card-clickable[data-product-id="${productId}"]`);
|
||||
|
||||
if (card) {
|
||||
const url = card.dataset.model3dUrl || card.dataset.modelUrl;
|
||||
if (url) return url;
|
||||
}
|
||||
|
||||
const handle = card?.dataset.productHandle;
|
||||
if (handle) {
|
||||
try {
|
||||
const res = await fetch(`/products/${encodeURIComponent(handle)}.js`);
|
||||
if (res.ok) {
|
||||
const product = await res.json();
|
||||
const media = (product.media || []).find(m => m.media_type === 'model');
|
||||
if (media) {
|
||||
const url = media.sources?.[0]?.url || media.src;
|
||||
if (url) return url;
|
||||
}
|
||||
const metaUrl = product.metafields?.custom?.model_3d_url;
|
||||
if (metaUrl) return metaUrl;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[3D] getModelUrl: product fetch failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return window.CUSTOMIZATION_3D_FALLBACK_MODEL_URL || null;
|
||||
};
|
||||
|
||||
const hotspot = {
|
||||
render: (items = []) => {
|
||||
if (!hasFn(kernel?.hotspot, 'render')) {
|
||||
console.warn('[3D] hotspot.render not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.hotspot.render(items);
|
||||
return true;
|
||||
},
|
||||
clear: () => {
|
||||
if (hasFn(kernel?.hotspot, 'render')) kernel.hotspot.render([]);
|
||||
},
|
||||
};
|
||||
|
||||
const material = {
|
||||
apply: (target, preset) => {
|
||||
if (!hasFn(kernel?.material, 'apply')) {
|
||||
console.warn('[3D] material.apply not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.material.apply({ target, material: preset });
|
||||
return true;
|
||||
},
|
||||
batch: (entries = []) => {
|
||||
if (!hasFn(kernel?.material, 'batch')) {
|
||||
console.warn('[3D] material.batch not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.material.batch(entries);
|
||||
return true;
|
||||
},
|
||||
reset: (target) => {
|
||||
if (!hasFn(kernel?.material, 'reset')) {
|
||||
console.warn('[3D] material.reset not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.material.reset(target);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const camera = {
|
||||
set: (config) => {
|
||||
if (!hasFn(kernel?.camera, 'set')) {
|
||||
console.warn('[3D] camera.set not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.camera.set(config);
|
||||
return true;
|
||||
},
|
||||
animateTo: (config, options) => {
|
||||
if (!hasFn(kernel?.camera, 'animateTo')) {
|
||||
console.warn('[3D] camera.animateTo not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.camera.animateTo(config, options);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const lights = {
|
||||
update: (name, config) => {
|
||||
if (!hasFn(kernel?.lights, 'update')) {
|
||||
console.warn('[3D] lights.update not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.lights.update(name, config);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const door = {
|
||||
toggle: (config = {}) => {
|
||||
if (!hasFn(kernel?.door, 'toggle')) {
|
||||
console.warn('[3D] door.toggle not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.door.toggle(config);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const clipping = {
|
||||
setY: (height, keepBelow = true, meshNames = []) => {
|
||||
if (!hasFn(kernel?.clipping, 'setY')) {
|
||||
console.warn('[3D] clipping.setY not available in current SDK version');
|
||||
return false;
|
||||
}
|
||||
kernel.clipping.setY(height, keepBelow, meshNames);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
init: initSDK,
|
||||
loadModel,
|
||||
clearModel,
|
||||
showEmpty,
|
||||
getModelUrl,
|
||||
getSDKCapability,
|
||||
isInitialized: () => sdkInitialized,
|
||||
getCurrentModelId: () => currentModelId,
|
||||
|
||||
hotspot,
|
||||
material,
|
||||
camera,
|
||||
lights,
|
||||
door,
|
||||
clipping,
|
||||
};
|
||||
})();
|
||||
|
||||
(function () {
|
||||
const tryInit = () => {
|
||||
if (document.querySelector('.customization-3d-wrapper')) {
|
||||
window.Customization3DViewer.init();
|
||||
}
|
||||
};
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => setTimeout(tryInit, 300));
|
||||
} else {
|
||||
setTimeout(tryInit, 300);
|
||||
}
|
||||
})();
|
||||
224
test/demo.html
Normal file
224
test/demo.html
Normal file
@ -0,0 +1,224 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D Viewer Demo</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.controls {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 10px 20px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.controls input {
|
||||
padding: 8px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.customization-3d-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-3d-container] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#preview-3d-viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[data-3d-loading] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
[data-3d-loading] .progress-wrapper {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
[data-3d-progress-bar] {
|
||||
width: 0%;
|
||||
height: 4px;
|
||||
background: #007bff;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
[data-3d-progress-text] {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-3d-empty] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>3D Viewer Demo</h1>
|
||||
|
||||
<div class="controls">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<input type="text" id="modelUrl" placeholder="输入模型 URL (GLB/GLTF)"
|
||||
value="https://sdk.zguiy.com/resurces/model/model.glb">
|
||||
</div>
|
||||
<button onclick="loadModel()">加载模型</button>
|
||||
<button onclick="clearModel()">清除模型</button>
|
||||
</div>
|
||||
|
||||
<div class="customization-3d-wrapper">
|
||||
<div data-3d-container>
|
||||
<div id="preview-3d-viewer"></div>
|
||||
</div>
|
||||
|
||||
<div data-3d-loading style="display:none;">
|
||||
<div class="progress-wrapper">
|
||||
<div data-3d-progress-bar></div>
|
||||
<div data-3d-progress-text>0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-3d-empty style="display:flex;">
|
||||
暂无模型,请加载一个 3D 模型
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SDK 脚本 -->
|
||||
<script src="https://sdk.zguiy.com/zt/assets/index.global.js"></script>
|
||||
|
||||
<!-- 3D 查看器核心 -->
|
||||
<script src="customization-3d-viewer.js"></script>
|
||||
|
||||
<!-- 3D 交互层 -->
|
||||
<script src="customization-3d-copy.js"></script>
|
||||
|
||||
<!-- 配置和交互脚本 -->
|
||||
<script>
|
||||
// 配置环境贴图
|
||||
window.CUSTOMIZATION_3D_ENV_URL = 'https://sdk.zguiy.com/resurces/hdr/hdr.env';
|
||||
|
||||
// 配置热点
|
||||
window.CUSTOMIZATION_3D_HOTSPOTS = [
|
||||
{
|
||||
id: "h1",
|
||||
name: "卷帘门",
|
||||
meshName: "Valve_01",
|
||||
icon: "https://bpic.588ku.com/element_pic/20/06/30/d1046b01afc0b9586844350d131f4daf.jpg!/fw/253/quality/90/unsharp/true/compress/true",
|
||||
offset: [25, 25, 0],
|
||||
radius: 20,
|
||||
color: "#21c7ff",
|
||||
payload: { type: "valve", code: "A" },
|
||||
},
|
||||
];
|
||||
|
||||
// 热点点击回调
|
||||
window.CUSTOMIZATION_3D_ON_HOTSPOT_CLICK = (detail, viewer) => {
|
||||
console.log('热点被点击:', detail);
|
||||
};
|
||||
|
||||
// 加载模型
|
||||
async function loadModel() {
|
||||
const modelUrl = document.getElementById('modelUrl').value.trim();
|
||||
|
||||
if (!modelUrl) {
|
||||
alert('请输入模型 URL');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.Customization3DInteractions.load3DModel(modelUrl, 'demo');
|
||||
} catch (err) {
|
||||
console.error('模型加载错误:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除模型
|
||||
async function clearModel() {
|
||||
try {
|
||||
await window.Customization3DInteractions.clear3DModel();
|
||||
} catch (err) {
|
||||
console.error('清除模型错误:', err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user