Appearance
3D Tiles 与模型样式
示例自带 MIT 授权的原创三角形,不需要第三方模型服务。替换为真实 tileset URL 时必须同时提供全部子内容,配置 HTTPS/CORS,并确认展示和再分发许可。可选 headers 仅用于受信任服务,不要把服务器秘密放入前端。
运行方法
下载独立 Demo ZIP,解压后运行 npm ci、npm run dev,打开 http://127.0.0.1:5174/recipes.html?example=model。无需修改代码或配置 Key。文件路径为 src/examples/model.ts。
页面与初始化(所有示例共用)
以下文件已包含在 ZIP 中。复制到自己的 Vite 项目时应同时保留这些文件与本页的操作文件,入口是 recipes.html。安装 SDK/Cesium 并复制资源的步骤见快速开始。
完整 HTML 与入口
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:," />
<title>ShardX Map · 可运行教程</title>
</head>
<body>
<nav id="recipes"></nav>
<div id="actions"></div>
<p id="status" role="status">正在加载…</p>
<div id="map"></div>
<script type="module" src="/src/recipes.ts"></script>
</body>
</html>ts
import { createContext } from './examples/runtime';
const recipes = {
markers: ['点、图片、DOM', () => import('./examples/markers')],
clusters: ['点聚合', () => import('./examples/clusters')],
geometry: ['线面样式', () => import('./examples/geometry')],
geofence: ['围栏与遮罩', () => import('./examples/geofence')],
model: ['3D Tiles', () => import('./examples/model')],
popups: ['弹窗与事件', () => import('./examples/popups')],
camera: ['相机、底图、昼夜', () => import('./examples/camera')],
lifecycle: ['多实例与销毁', () => import('./examples/lifecycle')],
} as const;
for (const [id, [title]] of Object.entries(recipes)) {
const link = document.createElement('a');
link.href = `?example=${id}`;
link.textContent = title;
document.querySelector('#recipes')!.append(link);
}
const id = new URLSearchParams(location.search).get('example') || 'markers';
async function start() {
if (!(id in recipes)) throw new Error('未知示例');
const ctx = await createContext();
try {
const [, load] = recipes[id as keyof typeof recipes];
await (await load()).setup(ctx);
ctx.report('示例已就绪');
document.body.dataset.ready = id;
} catch (error) {
ctx.dispose();
throw error;
}
}
void start().catch((error) => {
document.querySelector('#status')!.textContent = String(error);
});地图初始化、按钮、异常与清理
ts
import { createMap } from '@shardx-map/sdk';
import type { MapInstance } from '@shardx-map/sdk';
import '@shardx-map/sdk/style.css';
import 'cesium/Build/Cesium/Widgets/widgets.css';
import './recipes.css';
export const view = { longitude: 114.05, latitude: 22.55, height: 50000, pitch: -90 };
export const assetBaseUrl = new URL(`${import.meta.env.BASE_URL}cesium/`, location.origin).href;
export type Context = Awaited<ReturnType<typeof createContext>>;
export async function createContext() {
const cleanup: (() => void)[] = [];
const report = (value: unknown) => {
document.querySelector('#status')!.textContent = String(value);
};
const map = await createMap({
container: 'map',
assetBaseUrl,
source: { type: 'naturalEarth' },
view,
});
const dispose = () => {
map.destroy();
cleanup
.splice(0)
.reverse()
.forEach((fn) => fn());
};
cleanup.push(
map.on('error', (event) => {
document.querySelector<HTMLElement>('#status')!.dataset.error = 'true';
report(`${event.code}: ${event.message}`);
}),
);
const action = (title: string, run: () => unknown | Promise<unknown>) => {
const button = document.createElement('button');
button.textContent = title;
const click = async () => {
button.disabled = true;
try {
await run();
} catch (error) {
document.querySelector<HTMLElement>('#status')!.dataset.error = 'true';
report(error);
} finally {
button.disabled = false;
}
};
button.addEventListener('click', click);
document.querySelector('#actions')!.append(button);
cleanup.push(() => {
button.removeEventListener('click', click);
button.remove();
});
};
const track = (other: MapInstance) => cleanup.push(() => other.destroy());
window.addEventListener('pagehide', dispose, { once: true });
cleanup.push(() => window.removeEventListener('pagehide', dispose));
if (import.meta.hot) import.meta.hot.dispose(dispose);
return { map, cleanup, report, action, track, dispose };
}完整 CSS
css
html,
body {
margin: 0;
height: 100%;
font: 14px system-ui;
}
body {
display: flex;
flex-direction: column;
}
nav,
#actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px 16px;
}
nav a {
color: #1765cc;
padding: 4px;
}
button {
cursor: pointer;
padding: 8px 12px;
}
#status {
margin: 0;
padding: 8px 16px;
background: #eef3fa;
}
#map {
flex: 1;
min-height: 400px;
position: relative;
}
.map-label {
color: white;
background: #174e8c;
padding: 8px 12px;
border-radius: 6px;
}
.second-map {
position: fixed;
right: 12px;
bottom: 48px;
width: 40vw;
height: 40vh;
z-index: 10;
}完整操作代码
ts
import type { Context } from './runtime';
export async function setup({ map, action }: Context) {
// MIT original triangle; root and child content are both local HTTP(S) files.
const url = new URL(`${import.meta.env.BASE_URL}models/triangle/tileset.json`, location.origin)
.href;
await map.addTilesetLayer({ id: 'model', url, opacity: 1, maximumScreenSpaceError: 16 });
await map.flyToLayer('model');
action('半透明', () => map.updateTilesetStyle('model', { opacity: 0.5 }));
action('恢复不透明', () => map.updateTilesetStyle('model', { opacity: 1 }));
action('显示包围盒', () => map.updateTilesetStyle('model', { debugShowBoundingVolume: true }));
action('隐藏', () => map.setLayerVisible('model', false));
action('显示', () => map.setLayerVisible('model', true));
}模型数据完整保存在 ZIP 的 public/models/triangle/:
json
{
"asset": { "version": "1.1" },
"geometricError": 1000,
"root": {
"transform": [
-0.9131901651548396, -0.40753370690591556, 0, 0, 0.15628492116374004, -0.35019889287752864,
0.9235452121278074, 0, -0.37637580379365543, 0.8433724047909539, 0.38348955807923013, 0,
-2401796.676451099, 5381879.011404265, 2430809.8043941027, 1
],
"boundingVolume": { "sphere": [0, 0, 0, 1500] },
"geometricError": 0,
"refine": "ADD",
"content": { "uri": "mesh.gltf" }
}
}gltf
{
"asset": { "version": "2.0", "copyright": "ShardX Map example authors, MIT" },
"scene": 0,
"scenes": [{ "nodes": [0] }],
"nodes": [{ "mesh": 0 }],
"meshes": [{ "primitives": [{ "attributes": { "POSITION": 0 }, "material": 0 }] }],
"materials": [
{
"doubleSided": true,
"pbrMetallicRoughness": { "baseColorFactor": [1, 0.7, 0, 1] },
"extensions": { "KHR_materials_unlit": {} }
}
],
"extensionsUsed": ["KHR_materials_unlit"],
"buffers": [
{
"byteLength": 36,
"uri": "data:application/octet-stream;base64,AADIwwAAAAAAAMjDAADIQwAAAAAAAMjDAAAAAAAAFkQAAMhD"
}
],
"bufferViews": [{ "buffer": 0, "byteLength": 36 }],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 3,
"type": "VEC3",
"min": [-400, 0, -400],
"max": [400, 600, 400]
}
]
}异步初始化和操作异常会显示到状态栏;SDK 的异步资源错误通过 error 订阅显示。离开页面或 HMR 时执行清理队列、取消订阅并销毁地图,同时移除业务 DOM 监听器。