fix: v2.16 样板库画布每块可编辑+模型框污染根治

- 预览对话框打开即编辑态, 报价/下单入口保留
- 格尺寸贴外框方向联动等比缩放整窗, 任意格任意方向均可编辑
- 根因修复: computeModelBox 误含扇 bd 退化点致 W/H 与梃位/缩放参考边全错
- 悬停格淡高亮 + pointer 光标; HUD 标签改格宽/格高
- 验证: 格尺寸/梃位/贴边缩放/组合写回四项截图全过, ui-shots 6 页无报错
This commit is contained in:
HHY
2026-09-23 10:39:28 -07:00
parent 6a5a3d1ed3
commit 21760d1887
13 changed files with 108 additions and 41 deletions
+75 -25
View File
@@ -10,9 +10,9 @@
<span class="dim-input">H <el-input-number v-model="sizeH" :min="300" :step="10" size="small"
controls-position="right" style="width:104px" @change="onSizeInput" /></span>
<template v-if="selRegion">
<span class="dim-input">格W <el-input-number v-model="regW" :min="100" :step="10" size="small"
<span class="dim-input">格宽 <el-input-number v-model="regW" :min="100" :step="10" size="small"
controls-position="right" style="width:92px" @change="onRegionSize" /></span>
<span class="dim-input">格H <el-input-number v-model="regH" :min="100" :step="10" size="small"
<span class="dim-input">格高 <el-input-number v-model="regH" :min="100" :step="10" size="small"
controls-position="right" style="width:92px" @change="onRegionSize" /></span>
</template>
<template v-else-if="selected">
@@ -70,6 +70,7 @@ let moveDrag: null | { shapeIdx: number; spIdx: number; startPos: number; startX
const selected = ref<{ shapeIdx: number; spIdx: number } | null>(null)
// 细节编辑: 选中的玻璃格(可输入格宽高) ; selRegion 与 selected(梃) 互斥
const selRegion = ref<{ shapeIdx: number; ri: number } | null>(null)
let hoverRegion: { shapeIdx: number; ri: number } | null = null
const regW = ref(0)
const regH = ref(0)
const mulPos = ref(0)
@@ -121,36 +122,56 @@ function moveGroup(shapeIdx: number, axis: 'v' | 'h', pos: number, delta: number
return applied
}
/** 格尺寸编辑: 输入目标宽/高, 平移该格的边界中梃(优先右/下边界, 其次左/上) */
/** 找格在该方向的可动边界梃: 优先 pos1(右/下)侧, 其次 pos0(左/上)侧; 返回 {pos, sign} 或 null */
function findRegionEdge(sps: SplitLine[], axis: 'v' | 'h', bb: { x0: number; y0: number; x1: number; y1: number }) {
const pos1 = axis === 'v' ? bb.x1 : bb.y1
const e1 = sps.find(s => spAxis(s) === axis && Math.abs(spPos(s) - pos1) < 1.5)
if (e1) return { pos: spPos(e1), sign: 1 }
const pos0 = axis === 'v' ? bb.x0 : bb.y0
const e0 = sps.find(s => spAxis(s) === axis && Math.abs(spPos(s) - pos0) < 1.5)
if (e0) return { pos: spPos(e0), sign: -1 }
return null
}
/** 格尺寸编辑: 输入目标宽/高 → 平移边界中梃; 格两边都贴外框时联动等比缩放整窗 */
function onRegionSize() {
if (!selRegion.value || !dd) return
undoStack.push(JSON.stringify(dd))
const { shapeIdx, ri } = selRegion.value
const bb = regionBb(shapeIdx, ri)
if (!bb) return
const sps = dd.sm[shapeIdx]?.mm?.sp ?? []
const curW = bb.x1 - bb.x0, curH = bb.y1 - bb.y0
const targetW = Math.max(100, Math.round(regW.value))
const targetH = Math.max(100, Math.round(regH.value))
let moved = false
if (Math.abs(targetW - curW) >= 1) {
const edge = sps.find(s => spAxis(s) === 'v' && Math.abs(spPos(s) - bb.x1) < 1.5)
?? sps.find(s => spAxis(s) === 'v' && Math.abs(spPos(s) - bb.x0) < 1.5)
let changed = false
// 宽
let bb = regionBb(shapeIdx, ri)
if (!bb) { undoStack.pop(); return }
let cur = bb.x1 - bb.x0
if (Math.abs(targetW - cur) >= 1) {
const edge = findRegionEdge(sps, 'v', bb)
if (edge) {
const sign = Math.abs(spPos(edge) - bb.x1) < 1.5 ? 1 : -1
moveGroup(shapeIdx, 'v', spPos(edge), (targetW - curW) * sign)
moved = true
moveGroup(shapeIdx, 'v', edge.pos, (targetW - cur) * edge.sign)
changed = true
} else if (Math.abs(bb.x0 - modelBox.x0) < 1.5 && Math.abs(bb.x1 - modelBox.x1) < 1.5) {
// 格宽=整窗宽(两边界均为外框): 等比缩放整窗
resizeTo(Math.round((modelBox.x1 - modelBox.x0) * targetW / cur), Math.round(modelBox.y1 - modelBox.y0))
changed = true
}
}
if (Math.abs(targetH - curH) >= 1) {
const edge = sps.find(s => spAxis(s) === 'h' && Math.abs(spPos(s) - bb.y1) < 1.5)
?? sps.find(s => spAxis(s) === 'h' && Math.abs(spPos(s) - bb.y0) < 1.5)
// 高(宽调整可能已缩放整窗, 重新取格 bbox)
bb = regionBb(shapeIdx, ri)
if (!bb) { undoStack.pop(); return }
cur = bb.y1 - bb.y0
if (Math.abs(targetH - cur) >= 1) {
const edge = findRegionEdge(sps, 'h', bb)
if (edge) {
const sign = Math.abs(spPos(edge) - bb.y1) < 1.5 ? 1 : -1
moveGroup(shapeIdx, 'h', spPos(edge), (targetH - curH) * sign)
moved = true
moveGroup(shapeIdx, 'h', edge.pos, (targetH - cur) * edge.sign)
changed = true
} else if (Math.abs(bb.y0 - modelBox.y0) < 1.5 && Math.abs(bb.y1 - modelBox.y1) < 1.5) {
resizeTo(Math.round(modelBox.x1 - modelBox.x0), Math.round((modelBox.y1 - modelBox.y0) * targetH / cur))
changed = true
}
}
if (!moved) { syncRegionInputs(); ElMessage.warning('该格贴外框, 请先加分格梃再调整'); return }
if (!changed) { undoStack.pop(); syncRegionInputs(); return }
replayOne(shapeIdx)
computeModelBox()
syncSizeInputs()
@@ -501,6 +522,19 @@ function drawFrame(
}
ctx.textBaseline = 'alphabetic'
// 悬停格淡高亮(可编辑提示)
if (props.editable && hoverRegion && hoverRegion.shapeIdx === shapeIdx
&& !(selRegion.value && selRegion.value.shapeIdx === shapeIdx && selRegion.value.ri === hoverRegion.ri)) {
const r = rp.regions[hoverRegion.ri]
if (r) {
ctx.beginPath()
regionPoints(r).forEach((p, i) => (i ? ctx.lineTo(tx(p.x), ty(p.y)) : ctx.moveTo(tx(p.x), ty(p.y))))
ctx.closePath()
ctx.fillStyle = 'rgba(64,158,255,0.14)'
ctx.fill()
}
}
// 6) 选中玻璃格高亮: 蓝虚线框 + 格尺寸
if (selRegion.value && selRegion.value.shapeIdx === shapeIdx) {
const r = rp.regions[selRegion.value.ri]
@@ -751,12 +785,10 @@ function computeModelBox() {
if (!dd) return
const pts: Pt[] = []
for (const s of dd.sm) {
// 外框只由基底多边形决定; 扇 bd 序列化常退化为单点, 计入会把模型框拉大
for (const r of (s.polygon?.polygon ?? [])) {
for (const seg of r) { pts.push(seg.ps); pts.push(seg.pe) }
}
for (const sub of (s.sm ?? []) as SashShape[]) {
if (sub.bd) { pts.push(sub.bd.from, sub.bd.to) }
}
}
if (!pts.length) { modelBox = { x0: 0, y0: 0, x1: 1000, y1: 1000 }; return }
modelBox = bboxOf(pts)
@@ -827,12 +859,30 @@ function onMove(e: MouseEvent) {
}
return
}
// 光标提示
// 光标提示 + 悬停格高亮
if (props.editable && !tool.value && rect) {
const sx = e.clientX - rect.left, sy = e.clientY - rect.top
const hit = hitResizeHandle(sx, sy)
const m = hit ? null : hitMullion(sx, sy)
let hover: { shapeIdx: number; ri: number } | null = null
if (!hit && !m) {
const p = toModel(sx, sy)
let best = Infinity
for (let si = 0; si < replays.length; si++) {
const regions = replays[si].regions
for (let ri = 0; ri < regions.length; ri++) {
const pts = regionPoints(regions[ri])
if (pts.length < 3 || !pointInPoly(p, pts)) continue
const b = bboxOf(pts)
const area = (b.x1 - b.x0) * (b.y1 - b.y0)
if (area < best) { best = area; hover = { shapeIdx: si, ri } }
}
}
}
cvRef.value!.style.cursor = hit === 'wh' ? 'nwse-resize' : hit === 'w' ? 'ew-resize' : hit === 'h' ? 'ns-resize'
: hitMullion(sx, sy) ? 'move' : 'grab'
: m ? 'move' : hover ? 'pointer' : 'grab'
const changed = hover?.shapeIdx !== hoverRegion?.shapeIdx || hover?.ri !== hoverRegion?.ri
if (changed) { hoverRegion = hover; render() }
}
if (!drag) return
view.cx += e.clientX - drag.x