feat: initialize mini program map engine

This commit is contained in:
2026-03-19 15:58:48 +08:00
commit 03abe28d8c
49 changed files with 28584 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "地图",
"disableScroll": true
}

View File

@@ -0,0 +1,173 @@
import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine'
const INTERNAL_BUILD_VERSION = 'map-build-58'
let mapEngine: MapEngine | null = null
function getFallbackStageRect(): MapEngineStageRect {
const systemInfo = wx.getSystemInfoSync()
const width = Math.max(320, systemInfo.windowWidth - 20)
const height = Math.max(280, Math.floor(systemInfo.windowHeight * 0.66))
return {
width,
height,
left: 10,
top: 0,
}
}
Page({
data: {} as MapEngineViewState,
onLoad() {
mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
onData: (patch) => {
this.setData(patch)
},
})
this.setData(mapEngine.getInitialData())
},
onReady() {
this.measureStageAndCanvas()
},
onUnload() {
if (mapEngine) {
mapEngine.destroy()
mapEngine = null
}
},
measureStageAndCanvas() {
const page = this
const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
const fallbackRect = getFallbackStageRect()
const rect: MapEngineStageRect = {
width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
}
const currentEngine = mapEngine
if (!currentEngine) {
return
}
currentEngine.setStage(rect)
const canvasQuery = wx.createSelectorQuery().in(page)
canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
canvasQuery.exec((canvasRes) => {
const canvasRef = canvasRes[0] as any
if (!canvasRef || !canvasRef.node) {
page.setData({
statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
})
return
}
const dpr = wx.getSystemInfoSync().pixelRatio || 1
try {
currentEngine.attachCanvas(canvasRef.node, rect.width, rect.height, dpr)
} catch (error) {
page.setData({
statusText: `WebGL 初始化失败 (${INTERNAL_BUILD_VERSION})`,
})
}
})
}
const query = wx.createSelectorQuery().in(page)
query.select('.map-stage').boundingClientRect()
query.exec((res) => {
const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
applyStage(rect)
})
},
handleTouchStart(event: WechatMiniprogram.TouchEvent) {
if (mapEngine) {
mapEngine.handleTouchStart(event)
}
},
handleTouchMove(event: WechatMiniprogram.TouchEvent) {
if (mapEngine) {
mapEngine.handleTouchMove(event)
}
},
handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
if (mapEngine) {
mapEngine.handleTouchEnd(event)
}
},
handleTouchCancel() {
if (mapEngine) {
mapEngine.handleTouchCancel()
}
},
handleRecenter() {
if (mapEngine) {
mapEngine.handleRecenter()
}
},
handleRotateStep() {
if (mapEngine) {
mapEngine.handleRotateStep()
}
},
handleRotationReset() {
if (mapEngine) {
mapEngine.handleRotationReset()
}
},
handleSetManualMode() {
if (mapEngine) {
mapEngine.handleSetManualMode()
}
},
handleSetNorthUpMode() {
if (mapEngine) {
mapEngine.handleSetNorthUpMode()
}
},
handleSetHeadingUpMode() {
if (mapEngine) {
mapEngine.handleSetHeadingUpMode()
}
},
handleAutoRotateCalibrate() {
if (mapEngine) {
mapEngine.handleAutoRotateCalibrate()
}
},
})

View File

@@ -0,0 +1,152 @@
<view class="page">
<view class="page__header">
<view>
<view class="page__eyebrow">CMR MINI PROGRAM</view>
<view class="page__title">{{mapName}}</view>
</view>
<view class="page__badge">{{mapReadyText}}</view>
</view>
<view class="map-stage-wrap">
<view
class="map-stage"
catchtouchstart="handleTouchStart"
catchtouchmove="handleTouchMove"
catchtouchend="handleTouchEnd"
catchtouchcancel="handleTouchCancel"
>
<view class="map-content">
<canvas
id="mapCanvas"
type="webgl"
canvas-id="mapCanvas"
class="map-canvas map-canvas--base"
></canvas>
</view>
<view class="map-stage__crosshair"></view>
<view class="map-stage__overlay">
<view class="overlay-card">
<view class="overlay-card__label">WEBGL MAP ENGINE</view>
<view class="overlay-card__title">North Up / Heading Up / Manual</view>
<view class="overlay-card__desc">
地图北已经固定为正上方。现在支持手动旋转、北朝上、朝向朝上三种模式,并提供指北针用于校验朝向。
</view>
</view>
<view class="compass-widget">
<view class="compass-widget__ring">
<view class="compass-widget__north">N</view>
<view class="compass-widget__needle" style="transform: translateX(-50%) rotate({{compassNeedleDeg}}deg);"></view>
<view class="compass-widget__center"></view>
</view>
<view class="compass-widget__label">{{sensorHeadingText}}</view>
</view>
</view>
</view>
</view>
<scroll-view class="info-panel" scroll-y enhanced show-scrollbar="true">
<view class="info-panel__row">
<text class="info-panel__label">Build</text>
<text class="info-panel__value">{{buildVersion}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Renderer</text>
<text class="info-panel__value">{{renderMode}}</text>
</view>
<view class="info-panel__row info-panel__row--stack">
<text class="info-panel__label">Projection</text>
<text class="info-panel__value">{{projectionMode}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Heading Mode</text>
<text class="info-panel__value">{{orientationModeText}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Sensor Heading</text>
<text class="info-panel__value">{{sensorHeadingText}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">North Ref</text>
<text class="info-panel__value">{{northReferenceText}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Auto Source</text>
<text class="info-panel__value">{{autoRotateSourceText}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Calibration</text>
<text class="info-panel__value">{{autoRotateCalibrationText}}</text>
</view>
<view class="info-panel__row info-panel__row--stack">
<text class="info-panel__label">Tile URL</text>
<text class="info-panel__value">{{tileSource}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Zoom</text>
<text class="info-panel__value">{{zoom}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Rotation</text>
<text class="info-panel__value">{{rotationText}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Center Tile</text>
<text class="info-panel__value">{{centerText}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Tile Size</text>
<text class="info-panel__value">{{tileSizePx}}px</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Visible Tiles</text>
<text class="info-panel__value">{{visibleTileCount}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Ready Tiles</text>
<text class="info-panel__value">{{readyTileCount}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Memory Tiles</text>
<text class="info-panel__value">{{memoryTileCount}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Disk Tiles</text>
<text class="info-panel__value">{{diskTileCount}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Cache Hit</text>
<text class="info-panel__value">{{cacheHitRateText}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Disk Hits</text>
<text class="info-panel__value">{{diskHitCount}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Net Fetches</text>
<text class="info-panel__value">{{networkFetchCount}}</text>
</view>
<view class="info-panel__row info-panel__row--stack">
<text class="info-panel__label">Status</text>
<text class="info-panel__value">{{statusText}}</text>
</view>
<view class="control-row">
<view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view>
<view class="control-chip control-chip--secondary" bindtap="handleRotationReset">旋转归零</view>
</view>
<view class="control-row control-row--triple">
<view class="control-chip {{orientationMode === 'manual' ? 'control-chip--active' : ''}}" bindtap="handleSetManualMode">手动</view>
<view class="control-chip {{orientationMode === 'north-up' ? 'control-chip--active' : ''}}" bindtap="handleSetNorthUpMode">北朝上</view>
<view class="control-chip {{orientationMode === 'heading-up' ? 'control-chip--active' : ''}}" bindtap="handleSetHeadingUpMode">朝向朝上</view>
</view>
<view class="control-row" wx:if="{{orientationMode === 'heading-up'}}">
<view class="control-chip" bindtap="handleAutoRotateCalibrate">按当前方向校准</view>
</view>
<view class="control-row" wx:if="{{orientationMode === 'manual'}}">
<view class="control-chip" bindtap="handleRotateStep">旋转 +15°</view>
</view>
</scroll-view>
</view>

View File

@@ -0,0 +1,337 @@
.page {
height: 100vh;
padding: 20rpx 20rpx 24rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
background:
radial-gradient(circle at top left, #d8f3dc 0%, rgba(216, 243, 220, 0) 32%),
linear-gradient(180deg, #f7fbf2 0%, #eef6ea 100%);
color: #163020;
}
.page__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16rpx;
flex-shrink: 0;
}
.page__eyebrow {
font-size: 20rpx;
letter-spacing: 4rpx;
color: #5f7a65;
}
.page__title {
margin-top: 8rpx;
font-size: 44rpx;
font-weight: 600;
}
.page__badge {
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: #163020;
color: #f7fbf2;
font-size: 22rpx;
}
.map-stage-wrap {
position: relative;
width: 100%;
height: 66vh;
min-height: 520rpx;
max-height: 72vh;
flex-shrink: 0;
margin-bottom: 16rpx;
}
.map-stage {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border: 2rpx solid rgba(22, 48, 32, 0.08);
border-radius: 32rpx;
background: #dbeed4;
box-shadow: 0 18rpx 40rpx rgba(22, 48, 32, 0.08);
}
.map-content {
position: absolute;
inset: 0;
}
.map-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.map-canvas--base {
z-index: 1;
}
.map-stage__crosshair {
position: absolute;
left: 50%;
top: 50%;
width: 44rpx;
height: 44rpx;
transform: translate(-50%, -50%);
border: 3rpx solid rgba(255, 255, 255, 0.95);
border-radius: 50%;
box-shadow: 0 0 0 4rpx rgba(22, 48, 32, 0.2);
pointer-events: none;
z-index: 3;
}
.map-stage__crosshair::before,
.map-stage__crosshair::after {
content: '';
position: absolute;
background: rgba(255, 255, 255, 0.95);
}
.map-stage__crosshair::before {
left: 50%;
top: -18rpx;
width: 2rpx;
height: 76rpx;
transform: translateX(-50%);
}
.map-stage__crosshair::after {
left: -18rpx;
top: 50%;
width: 76rpx;
height: 2rpx;
transform: translateY(-50%);
}
.map-stage__overlay {
position: absolute;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 24rpx;
box-sizing: border-box;
pointer-events: none;
z-index: 4;
}
.overlay-card {
width: 68%;
padding: 22rpx;
border-radius: 24rpx;
background: rgba(247, 251, 242, 0.92);
box-shadow: 0 12rpx 30rpx rgba(22, 48, 32, 0.08);
}
.overlay-card__label {
font-size: 20rpx;
letter-spacing: 3rpx;
color: #5f7a65;
}
.overlay-card__title {
margin-top: 10rpx;
font-size: 34rpx;
font-weight: 600;
}
.overlay-card__desc {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.6;
color: #45624b;
}
.compass-widget {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
}
.compass-widget__ring {
position: relative;
width: 108rpx;
height: 108rpx;
border-radius: 50%;
background: rgba(247, 251, 242, 0.94);
border: 2rpx solid rgba(22, 48, 32, 0.12);
box-shadow: 0 10rpx 24rpx rgba(22, 48, 32, 0.1);
}
.compass-widget__north {
position: absolute;
left: 50%;
top: 10rpx;
transform: translateX(-50%);
font-size: 20rpx;
font-weight: 700;
color: #d62828;
}
.compass-widget__needle {
position: absolute;
left: 50%;
top: 18rpx;
width: 4rpx;
height: 72rpx;
transform-origin: 50% 36rpx;
background: linear-gradient(180deg, #d62828 0%, #163020 100%);
border-radius: 999rpx;
}
.compass-widget__center {
position: absolute;
left: 50%;
top: 50%;
width: 14rpx;
height: 14rpx;
transform: translate(-50%, -50%);
border-radius: 50%;
background: #163020;
}
.compass-widget__label {
min-width: 92rpx;
padding: 6rpx 10rpx;
border-radius: 999rpx;
background: rgba(247, 251, 242, 0.94);
font-size: 20rpx;
text-align: center;
color: #163020;
box-shadow: 0 8rpx 18rpx rgba(22, 48, 32, 0.08);
}
.info-panel {
flex: 1;
min-height: 0;
padding: 22rpx 20rpx 28rpx;
box-sizing: border-box;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 12rpx 32rpx rgba(22, 48, 32, 0.08);
}
.info-panel__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 10rpx 0;
border-bottom: 1rpx solid rgba(22, 48, 32, 0.08);
}
.info-panel__row--stack {
display: block;
}
.info-panel__row:last-of-type {
border-bottom: none;
}
.info-panel__label {
flex-shrink: 0;
font-size: 22rpx;
letter-spacing: 2rpx;
color: #5f7a65;
text-transform: uppercase;
}
.info-panel__value {
font-size: 25rpx;
color: #163020;
text-align: right;
word-break: break-all;
}
.info-panel__row--stack .info-panel__value {
display: block;
margin-top: 10rpx;
text-align: left;
color: #45624b;
line-height: 1.5;
}
.info-panel__actions {
display: flex;
gap: 14rpx;
margin-top: 18rpx;
}
.info-panel__actions--triple .info-panel__action {
font-size: 23rpx;
}
.info-panel__action {
flex: 1;
min-width: 0;
border-radius: 999rpx;
background: #d7e8da;
color: #163020;
font-size: 26rpx;
}
.info-panel__action--primary {
background: #2d6a4f;
color: #f7fbf2;
}
.info-panel__action--secondary {
background: #eef6ea;
color: #45624b;
}
.info-panel__action--active {
background: #2d6a4f;
color: #f7fbf2;
}
.control-row {
display: flex;
gap: 14rpx;
margin-top: 18rpx;
}
.control-row--triple .control-chip {
font-size: 23rpx;
}
.control-chip {
flex: 1;
min-width: 0;
padding: 20rpx 16rpx;
border-radius: 999rpx;
background: #d7e8da;
color: #163020;
font-size: 26rpx;
text-align: center;
box-sizing: border-box;
}
.control-chip--primary {
background: #2d6a4f;
color: #f7fbf2;
}
.control-chip--secondary {
background: #eef6ea;
color: #45624b;
}
.control-chip--active {
background: #2d6a4f;
color: #f7fbf2;
}