feat(map-editor): 站点/路径视口样式实时编辑与画布快捷键联动

- reflection API 新增 viewport-style GET/PATCH 及 ARGB 无符号化工具
- 属性面板「类型默认」重构为选中对象的视口样式实时编辑(防抖 + 序号防竞态)
- 画布 iframe 聚焦时经 SSE 转发撤销/重做/删除,移除冗余的坐标添加菜单

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-05-31 00:16:46 +08:00
co-authored by Cursor
parent 62943e6c42
commit 061acad9de
7 changed files with 580 additions and 214 deletions
@@ -243,6 +243,12 @@ async function del<T>(path: string): Promise<T> {
return data.data as T
}
async function patchJson<T>(path: string, body: unknown): Promise<T> {
const { data } = await http.patch<ReflectionEnvelope<T>>(`${BASE}${path}`, body)
if (!data?.success) throw new Error(data?.message ?? `reflection PATCH ${path} failed`)
return data.data as T
}
export const reflectionApi = {
listKinds: () => MOCK
? Promise.resolve(mockReflectionKinds())
@@ -500,6 +506,55 @@ export const reflectionApi = {
// ──────────────────────────────────────────────────────────────────────────
// 车型样式(WorkspaceCarStylesByType / WorkspaceAlarmColorScheme
// /viewport-style GET 站点/路径视口 Painter 样式
// /viewport-style PATCH 部分更新并立即重绘画布
getViewportStyle: () => MOCK
? Promise.resolve({
site: {
drawScale: 2.5,
dotRadiusNormal: 4,
dotRadiusSelected: 6,
selectionRingM: 0.14,
colorNormalArgb: 0xffffffff,
colorSelectedArgb: 0xffff0000,
labelColorNormalArgb: 0xffffffff,
labelColorSelectedArgb: 0xffff0000
},
track: {
drawScale: 1.5,
widthNormal: 1.2,
widthSelected: 2.5,
showDirectionArrows: true,
colorNormalArgb: 0xff00ffff,
colorSelectedArgb: 0xffff0000
}
} satisfies ViewportStylePayload)
: get<ViewportStylePayload>('/viewport-style'),
patchViewportStyle: (patch: ViewportStylePatch) => MOCK
? Promise.resolve({
site: {
drawScale: 2.5,
dotRadiusNormal: 4,
dotRadiusSelected: 6,
selectionRingM: 0.14,
colorNormalArgb: 0xffffffff,
colorSelectedArgb: 0xffff0000,
labelColorNormalArgb: 0xffffffff,
labelColorSelectedArgb: 0xffff0000
},
track: {
drawScale: 1.5,
widthNormal: 1.2,
widthSelected: 2.5,
showDirectionArrows: true,
colorNormalArgb: 0xff00ffff,
colorSelectedArgb: 0xffff0000
}
} satisfies ViewportStylePayload)
: patchJson<ViewportStylePayload>('/viewport-style', sanitizeViewportPatch(patch)),
// /car-style/types GET 列出所有 Car 子类型样式
// /car-style/{typeFullName} GET 单个车型当前样式
// /car-style/{typeFullName} POST body JSON 全量覆盖
@@ -551,6 +606,117 @@ export const reflectionApi = {
: post<{ path: string }>('/car-style/save')
}
/** 与 SimpleLite 视口样式对话框 / Configuration.conf.viewport 同步。 */
export interface ViewportStyleSite {
drawScale: number
dotRadiusNormal: number
dotRadiusSelected: number
selectionRingM: number
colorNormalArgb: number
colorSelectedArgb: number
labelColorNormalArgb: number
labelColorSelectedArgb: number
}
export interface ViewportStyleTrack {
drawScale: number
widthNormal: number
widthSelected: number
showDirectionArrows: boolean
colorNormalArgb: number
colorSelectedArgb: number
}
export interface ViewportStylePayload {
site: ViewportStyleSite
track: ViewportStyleTrack
}
export type ViewportStylePatch = {
site?: Partial<ViewportStyleSite>
track?: Partial<ViewportStyleTrack>
}
/** 将 ARGB 规范为无符号 32 位,避免 JSON 序列化成负数导致后端 UInt32 反序列化失败。 */
export function normalizeArgb(argb: number): number {
if (!Number.isFinite(argb)) return 0
return argb >>> 0
}
export function argbToHex(argb: number): string {
const u = normalizeArgb(argb)
const a = (u >>> 24) & 0xff
const r = (u >>> 16) & 0xff
const g = (u >>> 8) & 0xff
const b = u & 0xff
if (a >= 255) return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
return `#${a.toString(16).padStart(2, '0')}${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
}
export function hexToArgb(hex: string): number {
const h = hex.replace('#', '').trim()
if (h.length === 6) {
const r = parseInt(h.slice(0, 2), 16)
const g = parseInt(h.slice(2, 4), 16)
const b = parseInt(h.slice(4, 6), 16)
return normalizeArgb((0xff << 24) | (r << 16) | (g << 8) | b)
}
if (h.length === 8) {
const a = parseInt(h.slice(0, 2), 16)
const r = parseInt(h.slice(2, 4), 16)
const g = parseInt(h.slice(4, 6), 16)
const b = parseInt(h.slice(6, 8), 16)
return normalizeArgb((a << 24) | (r << 16) | (g << 8) | b)
}
return 0xffffffff
}
const VIEWPORT_SITE_ARGB_KEYS = [
'colorNormalArgb',
'colorSelectedArgb',
'labelColorNormalArgb',
'labelColorSelectedArgb'
] as const
const VIEWPORT_TRACK_ARGB_KEYS = ['colorNormalArgb', 'colorSelectedArgb'] as const
/** PATCH 前把视口样式中的 ARGB 字段转为 UInt32 可接受的非负整数。 */
export function sanitizeViewportPatch(patch: ViewportStylePatch): ViewportStylePatch {
const out: ViewportStylePatch = {}
if (patch.site) {
const site = { ...patch.site }
for (const k of VIEWPORT_SITE_ARGB_KEYS) {
if (site[k] != null) site[k] = normalizeArgb(site[k]!)
}
out.site = site
}
if (patch.track) {
const track = { ...patch.track }
for (const k of VIEWPORT_TRACK_ARGB_KEYS) {
if (track[k] != null) track[k] = normalizeArgb(track[k]!)
}
out.track = track
}
return out
}
export function normalizeViewportPayload(raw: ViewportStylePayload): ViewportStylePayload {
return {
site: {
...raw.site,
colorNormalArgb: normalizeArgb(raw.site.colorNormalArgb),
colorSelectedArgb: normalizeArgb(raw.site.colorSelectedArgb),
labelColorNormalArgb: normalizeArgb(raw.site.labelColorNormalArgb),
labelColorSelectedArgb: normalizeArgb(raw.site.labelColorSelectedArgb)
},
track: {
...raw.track,
colorNormalArgb: normalizeArgb(raw.track.colorNormalArgb),
colorSelectedArgb: normalizeArgb(raw.track.colorSelectedArgb)
}
}
}
export interface CarStyleDto {
bodyLengthM: number
bodyWidthM: number