Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions docs/.vitepress/theme/components/XingshuCampusMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ type SemanticLayer = {
sourceWayCount: number;
};

type ReferenceMap = {
uri: string;
bytes: number;
sha256: string;
source: string;
defaultVisible: boolean;
imageSizePx: [number, number];
fitRmsM: number;
pixelToLocalAffine: {
x: [number, number, number];
y: [number, number, number];
};
campusBoundaryLocalEN: Array<[number, number]>;
};

type CampusManifest = {
status: string;
presentation: {
Expand All @@ -36,6 +51,7 @@ type CampusManifest = {
triangles: number;
featureCount: number;
};
referenceMap?: ReferenceMap;
semanticLayers: SemanticLayer[];
features: CampusFeature[];
attribution: Array<{
Expand All @@ -57,6 +73,9 @@ const isFullscreen = ref(false);
const showGround = ref(true);
const showCampusRoads = ref(true);
const showOuterRoads = ref(true);
const showReferenceMap = ref(false);
const referenceLoading = ref(false);
const referenceError = ref('');

const features = computed(() =>
[...(manifest.value?.features ?? [])].sort((left, right) =>
Expand All @@ -80,6 +99,9 @@ let animationFrame = 0;
let modelRoot: THREE.Object3D | undefined;
let campusBounds: THREE.Box3 | undefined;
let selectionHelper: THREE.Box3Helper | undefined;
let manifestAssetUrl: URL | undefined;
let referenceMapMesh: THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial> | undefined;
let referenceMapTexture: THREE.Texture | undefined;
const featureRoots = new Map<string, THREE.Object3D>();
const semanticNodes = new Map<string, THREE.Object3D>();
const selectableMeshes: THREE.Mesh[] = [];
Expand Down Expand Up @@ -180,6 +202,111 @@ function applyLayerVisibility() {
if (outerRoads) outerRoads.visible = showOuterRoads.value;
}

function pointInBoundary(point: [number, number], boundary: Array<[number, number]>) {
let inside = false;
for (let index = 0, previous = boundary.length - 1; index < boundary.length; previous = index++) {
const [currentX, currentY] = boundary[index];
const [previousX, previousY] = boundary[previous];
const crosses =
currentY > point[1] !== previousY > point[1] &&
point[0] <
((previousX - currentX) * (point[1] - currentY)) / (previousY - currentY) + currentX;
if (crosses) inside = !inside;
}
return inside;
}

async function createReferenceMap() {
const reference = manifest.value?.referenceMap;
if (!reference || !manifestAssetUrl || !renderer || !scene) return undefined;

const textureUrl = new URL(reference.uri, manifestAssetUrl).href;
const texture = await new THREE.TextureLoader().loadAsync(textureUrl);
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
referenceMapTexture = texture;

const positions: number[] = [];
const uvs: number[] = [];
const cells = 28;
const [width, height] = reference.imageSizePx;
const mapPixel = (pixelX: number, pixelY: number): [number, number] => [
reference.pixelToLocalAffine.x[0] * pixelX +
reference.pixelToLocalAffine.x[1] * pixelY +
reference.pixelToLocalAffine.x[2],
reference.pixelToLocalAffine.y[0] * pixelX +
reference.pixelToLocalAffine.y[1] * pixelY +
reference.pixelToLocalAffine.y[2],
];

for (let row = 0; row < cells; row += 1) {
for (let column = 0; column < cells; column += 1) {
const x0 = (column / cells) * width;
const x1 = ((column + 1) / cells) * width;
const y0 = (row / cells) * height;
const y1 = ((row + 1) / cells) * height;
const center = mapPixel((x0 + x1) / 2, (y0 + y1) / 2);
if (!pointInBoundary(center, reference.campusBoundaryLocalEN)) continue;

const corners: Array<[number, number]> = [
[x0, y0],
[x1, y0],
[x1, y1],
[x0, y1],
];
const mapped = corners.map(([pixelX, pixelY]) => mapPixel(pixelX, pixelY));
for (const cornerIndex of [0, 1, 2, 0, 2, 3]) {
const [localX, localNorth] = mapped[cornerIndex];
const [pixelX, pixelY] = corners[cornerIndex];
positions.push(localX, 0.16, -localNorth);
uvs.push(pixelX / width, 1 - pixelY / height);
}
}
}

const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
const material = new THREE.MeshBasicMaterial({
map: texture,
opacity: 0.56,
polygonOffset: true,
polygonOffsetFactor: -3,
polygonOffsetUnits: -3,
side: THREE.DoubleSide,
transparent: true,
depthTest: true,
depthWrite: false,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.name = 'reference-campus-map';
mesh.renderOrder = 12;
mesh.visible = false;
scene.add(mesh);
return mesh;
}

async function applyReferenceMapVisibility() {
if (!showReferenceMap.value) {
if (referenceMapMesh) referenceMapMesh.visible = false;
return;
}
if (!manifest.value?.referenceMap || !scene) return;

referenceLoading.value = true;
referenceError.value = '';
try {
referenceMapMesh ??= await createReferenceMap();
if (!referenceMapMesh) throw new Error('参考图配置不完整');
referenceMapMesh.visible = true;
} catch (error) {
referenceError.value = error instanceof Error ? error.message : '参考图加载失败';
showReferenceMap.value = false;
} finally {
referenceLoading.value = false;
}
}

function resizeRenderer() {
if (!host.value || !renderer || !camera) return;
const width = Math.max(1, host.value.clientWidth);
Expand Down Expand Up @@ -228,9 +355,11 @@ async function initializeMap() {
if (!host.value) return;
try {
const manifestUrl = new URL(withBase('/maps/xingshu-campus/manifest.json'), location.href);
manifestAssetUrl = manifestUrl;
const response = await fetch(manifestUrl);
if (!response.ok) throw new Error(`地图清单加载失败(HTTP ${response.status})`);
manifest.value = (await response.json()) as CampusManifest;
showReferenceMap.value = manifest.value.referenceMap?.defaultVisible ?? false;
if (manifest.value.presentation.renderedRoofCards) {
throw new Error('地图包意外包含屋顶字牌,已停止加载。');
}
Expand Down Expand Up @@ -334,6 +463,12 @@ function disposeMap() {
}
controls?.dispose();
if (selectionHelper) disposeHelper(selectionHelper);
if (referenceMapMesh) {
scene?.remove(referenceMapMesh);
referenceMapMesh.geometry.dispose();
referenceMapMesh.material.dispose();
}
referenceMapTexture?.dispose();
for (const outline of generatedOutlines) outline.geometry.dispose();
outlineMaterial.dispose();
modelRoot?.traverse((object) => {
Expand All @@ -346,6 +481,7 @@ function disposeMap() {
}

watch([showGround, showCampusRoads, showOuterRoads], applyLayerVisibility);
watch(showReferenceMap, () => void applyReferenceMapVisibility());
onMounted(initializeMap);
onBeforeUnmount(disposeMap);
</script>
Expand Down Expand Up @@ -408,7 +544,16 @@ onBeforeUnmount(disposeMap);
<label><input v-model="showGround" type="checkbox" /> 地面与水体</label>
<label><input v-model="showCampusRoads" type="checkbox" /> 校内道路</label>
<label><input v-model="showOuterRoads" type="checkbox" /> 外围道路</label>
<label title="默认关闭,仅供目视参考">
<input
v-model="showReferenceMap"
type="checkbox"
:disabled="!isReady || referenceLoading || !manifest?.referenceMap"
/>
{{ referenceLoading ? '参考原图加载中' : '参考原图' }}
</label>
</fieldset>
<span v-if="referenceError" class="xingshu-map-layer-error">{{ referenceError }}</span>
<div class="xingshu-map-stats">
<span>{{ manifest?.model.featureCount ?? 0 }} 栋</span>
<span>{{ manifest?.model.triangles.toLocaleString() ?? '—' }} 三角面</span>
Expand Down Expand Up @@ -669,6 +814,17 @@ onBeforeUnmount(disposeMap);
accent-color: var(--map-seal);
}

.xingshu-map-layers input:disabled {
cursor: wait;
opacity: 0.55;
}

.xingshu-map-layer-error {
color: var(--map-seal);
font-size: 10px;
font-weight: 700;
}

.xingshu-map-stats {
grid-column: 1 / -1;
}
Expand Down
39 changes: 37 additions & 2 deletions docs/public/maps/xingshu-campus/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"datasetId": "cwnu-xingshu",
"releaseId": "s14-site-draft",
"releaseId": "s14-site-reference",
"status": "needs-review",
"generatedAt": "2026-08-02T13:19:46.602Z",
"coordinateSystem": {
Expand Down Expand Up @@ -33,6 +33,41 @@
"meshes": 50,
"featureCount": 42
},
"referenceMap": {
"uri": "xingshu-campus-reference.png",
"bytes": 1574946,
"sha256": "906de61faf9b6ddb85618ebfacfa0c116a02786635b9b1329893064f1b5e3885",
"source": "网络来源",
"defaultVisible": false,
"imageSizePx": [1280, 1329],
"fitRmsM": 6.297,
"pixelToLocalAffine": {
"x": [0.497944055, 0.019691733, -303.734147],
"y": [-0.0141137944, -0.481156996, 602.420555]
},
"campusBoundaryLocalEN": [
[-143.5301025105873, 566.8832062925212],
[-206.76443840039428, 369.2943924115971],
[-237.3207488671178, 251.24063476361334],
[-267.3956757232081, 136.76076205493882],
[0, 0],
[39.35447751090396, 113.40685947332531],
[202.3825763502391, 69.4223837070167],
[251.08777147287037, 225.61720975954086],
[201.57798497809563, 237.92723582871258],
[230.5007169349119, 337.5113919307478],
[190.3230805749772, 349.52420246461406],
[152.9643743833294, 360.9327372414991],
[161.519735801965, 395.2437522010878],
[127.6297509855358, 436.6909156870097],
[116.94227564590983, 462.3356916070916],
[76.53639700484928, 473.2826128178276],
[25.46947207581252, 420.3812082638033],
[-12.552449640934356, 435.10912372218445],
[34.47880723909475, 518.3972451174632],
[-109.13522775517777, 587.7331380210817]
]
},
"semanticLayers": [
{
"id": "campus-ground",
Expand Down Expand Up @@ -684,6 +719,6 @@
"limitations": [
"学生维护的非官方白模,不是测绘成果,也不能代替校园导航或应急疏散图。",
"建筑名称、用途、轮廓与高度仍有待核验项;请以学校现场标识和正式通知为准。",
"模型不包含全景图片、功能图、校徽、个人信息或任何受限参考图层。"
"模型不包含全景图片、校徽或个人信息;参考原图默认关闭且仅用于目视参考。"
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/start/xingshu-campus-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ sources:

- 本图不是学校发布的校园导航图、测绘成果或应急疏散图。
- 建筑名称、用途、轮廓、高度和现状仍有待核验项,请以现场标识和学校正式通知为准。
- 模型不包含全景图片、校徽、功能图、水印、个人信息或受限参考图层
- 模型不包含全景图片、校徽或个人信息;参考原图默认关闭且仅用于目视参考
- 可发布道路与地面语义主要依据 OpenStreetMap 矢量数据,并保留贡献者署名。
- 为避免遮挡和方向错误,建筑名称只显示在选择器与信息面板,不放置屋顶字牌。

## 模型范围

当前版本包含 42 栋可点选白模、校内与外围道路、运动场、水体和校园地面。
模型采用宣纸水墨风格,主要用于新生熟悉空间关系和后续共同校对。
模型采用宣纸水墨风格,并提供默认关闭的参考原图图层,主要用于新生熟悉空间关系和后续共同校对。

如果发现建筑缺失、名称错误或位置明显偏差,欢迎通过页面底部的 GitHub 编辑入口提交修正。
Loading