feat: 渲染视觉与图纸对齐(源站原生扇内结构全量消费: 扇内分割/裙板/格条/编号, inferSashPanels 工具+三渲染器接入+锚点归属匹配+对话框ResizeObserver)
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
* 在 2D 窗型几何上叠加玻璃渐变/高光/投影/墙面光影,输出照片观感。
|
||||
*/
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { bboxOf, parseDrawing, regionPoints, replayShape, type DrawingData } from '../types/drawing'
|
||||
import { bboxOf, inferSashGlyphs, inferSashPanels, parseDrawing, regionPoints, replayShape, type DrawingData, type FrameShape, type SashShape } from '../types/drawing'
|
||||
|
||||
const props = defineProps<{ drawingData: string | object; tint?: string }>()
|
||||
const cvRef = ref<HTMLCanvasElement>()
|
||||
@@ -81,6 +81,126 @@ function render() {
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
}
|
||||
// 扇层:扇内子格(裙板/格条/编号) + 扇料框 + 开向符号 + 把手(语义同 WindoorCanvas,视觉对齐 2D 图纸)
|
||||
const rp = replayShape(shape)
|
||||
const glyphs = inferSashGlyphs(shape, rp).sashes
|
||||
const sashShapes: SashShape[] = []
|
||||
const collect = (s: FrameShape | SashShape) => {
|
||||
if (!['Frame', 'Slide'].includes(String(s.type))) sashShapes.push(s as SashShape)
|
||||
;(((s as FrameShape).sm ?? []) as (FrameShape | SashShape)[]).forEach(collect)
|
||||
;(((s as SashShape).sashes ?? []) as SashShape[]).forEach(collect)
|
||||
}
|
||||
collect(shape)
|
||||
glyphs.forEach((g, gi) => {
|
||||
const bx0 = tx(g.x0), bx1 = tx(g.x1), by0 = ty(g.y0), by1 = ty(g.y1)
|
||||
const w = bx1 - bx0, h = by1 - by0
|
||||
if (w < 6 || h < 6) return
|
||||
// 子格:锚点归属匹配扇形状 → 玻璃格/裙板/格条/编号
|
||||
const hit = sashShapes.find(s => (s.mm?.sp ?? []).some(sp => {
|
||||
const pt = sp.line?.pt
|
||||
return pt && pt.x >= g.x0 && pt.x <= g.x1 && pt.y >= g.y0 && pt.y <= g.y1
|
||||
})) ?? sashShapes[gi % Math.max(1, sashShapes.length)]
|
||||
if (hit) {
|
||||
for (const p of inferSashPanels(hit, g)) {
|
||||
const px0 = tx(p.x0), px1 = tx(p.x1), py0 = ty(p.y0), py1 = ty(p.y1)
|
||||
if (px1 - px0 < 4 || py1 - py0 < 4) continue
|
||||
if (!p.glass) {
|
||||
// 裙板/实板:暖米色 + 内凹描边
|
||||
ctx.fillStyle = '#ddd3c0'
|
||||
ctx.fillRect(px0, py0, px1 - px0, py1 - py0)
|
||||
ctx.strokeStyle = 'rgba(90,84,72,0.55)'
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.strokeRect(px0 + 3, py0 + 3, px1 - px0 - 6, py1 - py0 - 6)
|
||||
} else if (p.deco) {
|
||||
// 源站格条(decorationChinese 等): hc 等分主格 + gp 内框 + 四角回纹块
|
||||
const bw = Math.max(1.2, 14 * scale)
|
||||
ctx.strokeStyle = 'rgba(72,58,40,0.7)'
|
||||
ctx.lineWidth = bw
|
||||
const inner = Math.max(6, p.deco.gp * scale)
|
||||
const w2 = px1 - px0, h2 = py1 - py0
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.rect(px0, py0, w2, h2)
|
||||
ctx.clip()
|
||||
// 内圈框
|
||||
ctx.strokeRect(px0 + inner, py0 + inner, w2 - 2 * inner, h2 - 2 * inner)
|
||||
// 主格等分线(hc)
|
||||
const hc = Math.max(2, p.deco.hc)
|
||||
for (let i = 1; i < hc; i++) {
|
||||
ctx.beginPath(); ctx.moveTo(px0 + (w2 * i) / hc, py0 + inner); ctx.lineTo(px0 + (w2 * i) / hc, py1 - inner); ctx.stroke()
|
||||
ctx.beginPath(); ctx.moveTo(px0 + inner, py0 + (h2 * i) / hc); ctx.lineTo(px1 - inner, py0 + (h2 * i) / hc); ctx.stroke()
|
||||
}
|
||||
// 四角回纹块(嵌套双框)
|
||||
const qs = Math.min(inner * 0.72, w2 * 0.14, h2 * 0.14)
|
||||
for (const [qx, qy] of [[px0 + inner, py0 + inner], [px1 - inner, py0 + inner], [px0 + inner, py1 - inner], [px1 - inner, py1 - inner]] as const) {
|
||||
const sx = qx === px0 + inner ? 1 : -1, sy = qy === py0 + inner ? 1 : -1
|
||||
ctx.strokeRect(Math.min(qx, qx + sx * qs), Math.min(qy, qy + sy * qs), qs, qs)
|
||||
ctx.strokeRect(Math.min(qx, qx + sx * qs * 0.55), Math.min(qy, qy + sy * qs * 0.55), qs * 0.55, qs * 0.55)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
// 玻璃/板编号(对齐图纸 A1/A2...)
|
||||
if (p.glassText) {
|
||||
const fs = Math.max(11, Math.min(18, (px1 - px0) * 0.09))
|
||||
ctx.fillStyle = p.glass ? 'rgba(255,255,255,0.85)' : 'rgba(90,84,72,0.8)'
|
||||
ctx.font = `bold ${fs}px sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(p.glassText, (px0 + px1) / 2, (py0 + py1) / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 扇料框
|
||||
ctx.strokeStyle = '#4a4e52'
|
||||
ctx.lineWidth = 3.2
|
||||
ctx.strokeRect(bx0, by0, w, h)
|
||||
const cx = (bx0 + bx1) / 2, cy = (by0 + by1) / 2
|
||||
// 开向符号: 平开=虚线三角(apex 指铰链侧), 推拉=水平滑行箭头
|
||||
ctx.strokeStyle = 'rgba(60,66,72,0.75)'
|
||||
ctx.lineWidth = 1.4
|
||||
if (g.kind === 'sliding') {
|
||||
const arrowDir = gi % 2 === 0 ? 1 : -1
|
||||
const L = Math.min(w, h) * 0.3
|
||||
ctx.setLineDash([])
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx - L * arrowDir, cy)
|
||||
ctx.lineTo(cx + L * arrowDir, cy)
|
||||
ctx.lineTo(cx + (L - 9) * arrowDir, cy - 6)
|
||||
ctx.moveTo(cx + L * arrowDir, cy)
|
||||
ctx.lineTo(cx + (L - 9) * arrowDir, cy + 6)
|
||||
ctx.stroke()
|
||||
} else {
|
||||
ctx.setLineDash([7, 5])
|
||||
ctx.beginPath()
|
||||
const dir = g.openDirection ?? 'left'
|
||||
if (dir.startsWith('up')) {
|
||||
ctx.moveTo(bx0, by1); ctx.lineTo(cx, by0); ctx.lineTo(bx1, by1)
|
||||
} else if (dir.startsWith('right')) {
|
||||
ctx.moveTo(bx0, by0); ctx.lineTo(bx1, cy); ctx.lineTo(bx0, by1)
|
||||
} else {
|
||||
ctx.moveTo(bx1, by0); ctx.lineTo(bx0, cy); ctx.lineTo(bx1, by1)
|
||||
}
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
}
|
||||
// 把手: 铰链对面侧白条
|
||||
const dir = g.openDirection ?? 'left'
|
||||
const hl = Math.max(10, h * (g.isDoor ? 0.34 : 0.2))
|
||||
const hw2 = Math.max(4, 5)
|
||||
ctx.fillStyle = '#f4f4f6'
|
||||
ctx.strokeStyle = '#3c4147'
|
||||
ctx.lineWidth = 1
|
||||
ctx.beginPath()
|
||||
if (g.kind === 'casement') {
|
||||
if (dir.startsWith('left')) ctx.rect(bx1 - hw2 - 3, cy - hl / 2, hw2, hl)
|
||||
else if (dir.startsWith('right')) ctx.rect(bx0 + 3, cy - hl / 2, hw2, hl)
|
||||
else ctx.rect(cx - hl / 2, by1 - hw2 - 3, hl, hw2)
|
||||
} else {
|
||||
ctx.rect(cx - hw2 / 2, cy - hl / 2, hw2, hl)
|
||||
}
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
})
|
||||
// 外框
|
||||
const all = dd.sm.flatMap(s => replayShape(s).regions.flat().map(seg => seg.ps))
|
||||
void all
|
||||
|
||||
@@ -119,10 +119,10 @@
|
||||
* - 改梃: 直接点击中梃选中(高亮) → 拖动移动(line.pt 更新后重放) → 删除梃或 Del 键删除
|
||||
* - 撤销栈; exportDrawing() 取回 drawing_data; change 事件通知父组件
|
||||
*/
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
bboxOf, DEC_PRESETS, inferSashGlyphs, parseDrawing, regionPoints, replayShape, spAxis, spPos,
|
||||
bboxOf, DEC_PRESETS, inferSashGlyphs, inferSashPanels, parseDrawing, regionPoints, replayShape, spAxis, spPos,
|
||||
type DecBar, type DrawingData, type FrameShape, type Pt, type SashAssign, type Segment, type SashShape, type SplitLine,
|
||||
} from '../types/drawing'
|
||||
|
||||
@@ -392,6 +392,7 @@ function hitMullion(sx: number, sy: number): { shapeIdx: number; spIdx: number }
|
||||
function render() {
|
||||
const cv = cvRef.value, wrap = wrapRef.value
|
||||
if (!cv || !wrap) return
|
||||
if (!wrap.clientWidth || !wrap.clientHeight) return // 容器未布局(dialog 动画/隐藏)时跳过, 待 ResizeObserver 触发
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
cv.width = wrap.clientWidth * dpr
|
||||
cv.height = wrap.clientHeight * dpr
|
||||
@@ -570,14 +571,72 @@ function drawFrame(
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 扇: 扇料框 + 官方开向符号 + 把手
|
||||
// 4) 扇: 扇内子格(源站 Panel 裙板/格条/编号) + 扇料框 + 官方开向符号 + 把手
|
||||
const glyphs = inferSashGlyphs(shape, rp).sashes
|
||||
const ewAvg = ((shape.fm?.ew ?? [60, 60, 60, 60]).reduce((a, b) => a + b, 0)) / 4
|
||||
const fw = Math.max(2.5, Math.min(lw, ewAvg * scale))
|
||||
const sashShapes: SashShape[] = []
|
||||
const collectSash = (s: FrameShape | SashShape) => {
|
||||
if (!['Frame', 'Slide'].includes(String(s.type))) sashShapes.push(s as SashShape)
|
||||
;(((s as FrameShape).sm ?? []) as (FrameShape | SashShape)[]).forEach(collectSash)
|
||||
;(((s as SashShape).sashes ?? []) as SashShape[]).forEach(collectSash)
|
||||
}
|
||||
collectSash(shape)
|
||||
glyphs.forEach((g, gi) => {
|
||||
const bx0 = tx(g.x0), bx1 = tx(g.x1), by0 = ty(g.y0), by1 = ty(g.y1)
|
||||
const w = bx1 - bx0, h = by1 - by0
|
||||
if (w < 6 || h < 6) return
|
||||
// 扇内子格: 锚点归属匹配 → 裙板实色/格条网格/编号(对齐官方图纸 A1/A2...)
|
||||
const hit = sashShapes.find(s => (s.mm?.sp ?? []).some(sp => {
|
||||
const pt = sp.line?.pt
|
||||
return pt && pt.x >= g.x0 && pt.x <= g.x1 && pt.y >= g.y0 && pt.y <= g.y1
|
||||
})) ?? sashShapes[gi % Math.max(1, sashShapes.length)]
|
||||
if (hit) {
|
||||
for (const p of inferSashPanels(hit, g)) {
|
||||
const px0 = tx(p.x0), px1 = tx(p.x1), py0 = ty(p.y0), py1 = ty(p.y1)
|
||||
if (px1 - px0 < 4 || py1 - py0 < 4) continue
|
||||
if (!p.glass) {
|
||||
ctx.fillStyle = '#e5dfd3'
|
||||
ctx.fillRect(px0, py0, px1 - px0, py1 - py0)
|
||||
ctx.strokeStyle = STYLE.profileDark
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeRect(px0 + 2, py0 + 2, px1 - px0 - 4, py1 - py0 - 4)
|
||||
} else if (p.deco) {
|
||||
// hc 等分主格 + gp 内框 + 四角回纹块(近似源站 decorationChinese)
|
||||
const inner = Math.max(5, p.deco.gp * scale)
|
||||
const w2 = px1 - px0, h2 = py1 - py0
|
||||
ctx.save()
|
||||
ctx.strokeStyle = 'rgba(58,64,72,0.6)'
|
||||
ctx.lineWidth = 1.2
|
||||
ctx.beginPath()
|
||||
ctx.rect(px0, py0, w2, h2)
|
||||
ctx.clip()
|
||||
ctx.strokeRect(px0 + inner, py0 + inner, w2 - 2 * inner, h2 - 2 * inner)
|
||||
const hc = Math.max(2, p.deco.hc)
|
||||
for (let i = 1; i < hc; i++) {
|
||||
ctx.beginPath(); ctx.moveTo(px0 + (w2 * i) / hc, py0 + inner); ctx.lineTo(px0 + (w2 * i) / hc, py1 - inner); ctx.stroke()
|
||||
ctx.beginPath(); ctx.moveTo(px0 + inner, py0 + (h2 * i) / hc); ctx.lineTo(px1 - inner, py0 + (h2 * i) / hc); ctx.stroke()
|
||||
}
|
||||
const qs = Math.min(inner * 0.72, w2 * 0.14, h2 * 0.14)
|
||||
for (const [qx, qy] of [[px0 + inner, py0 + inner], [px1 - inner, py0 + inner], [px0 + inner, py1 - inner], [px1 - inner, py1 - inner]] as const) {
|
||||
const sx = qx === px0 + inner ? 1 : -1, sy = qy === py0 + inner ? 1 : -1
|
||||
ctx.strokeRect(Math.min(qx, qx + sx * qs), Math.min(qy, qy + sy * qs), qs, qs)
|
||||
ctx.strokeRect(Math.min(qx, qx + sx * qs * 0.55), Math.min(qy, qy + sy * qs * 0.55), qs * 0.55, qs * 0.55)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
if (p.glassText) {
|
||||
const fs2 = Math.max(9, Math.min(14, (px1 - px0) * 0.1))
|
||||
ctx.fillStyle = p.glass ? 'rgba(255,255,255,0.92)' : STYLE.label
|
||||
ctx.font = `bold ${fs2}px sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(p.glassText, (px0 + px1) / 2, (py0 + py1) / 2)
|
||||
ctx.textAlign = 'left'
|
||||
ctx.textBaseline = 'alphabetic'
|
||||
}
|
||||
}
|
||||
}
|
||||
// 扇料框(细一档的型材)
|
||||
drawProfileRect(ctx, bx0, by0, w, h, fw)
|
||||
const cx = (bx0 + bx1) / 2, cy = (by0 + by1) / 2
|
||||
@@ -1207,6 +1266,17 @@ onMounted(() => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
window.addEventListener('resize', render)
|
||||
// 容器尺寸追踪: el-dialog 内挂载时首帧可能 0 尺寸, 布局完成后自动重渲染
|
||||
if (wrapRef.value) {
|
||||
renderRo = new ResizeObserver(() => render())
|
||||
renderRo.observe(wrapRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
let renderRo: ResizeObserver | null = null
|
||||
onBeforeUnmount(() => {
|
||||
renderRo?.disconnect()
|
||||
window.removeEventListener('resize', render)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ let ro: ResizeObserver | null = null
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as THREE from 'three'
|
||||
import {
|
||||
bboxOf, inferSashGlyphs, parseDrawing, regionPoints, replayShape,
|
||||
type DecBar, type DrawingData, type Pt,
|
||||
bboxOf, inferSashGlyphs, inferSashPanels, parseDrawing, regionPoints, replayShape,
|
||||
type DecBar, type DrawingData, type FrameShape, type Pt, type SashShape,
|
||||
} from '../types/drawing'
|
||||
import { profileGeo } from '../three/profileGeo'
|
||||
|
||||
@@ -256,6 +256,14 @@ function build(drawing: DrawingData) {
|
||||
for (const shape of drawing.sm) {
|
||||
const { regions, sashes } = inferSashGlyphs(shape)
|
||||
if (!regions.length) continue
|
||||
// 收集 Sash 级形状, 供扇内子格(裙板/格条)锚点归属匹配
|
||||
const sashShapes3d: SashShape[] = []
|
||||
const collectSash3d = (s: FrameShape | SashShape) => {
|
||||
if (!['Frame', 'Slide'].includes(String(s.type))) sashShapes3d.push(s as SashShape)
|
||||
;(((s as FrameShape).sm ?? []) as (FrameShape | SashShape)[]).forEach(x => collectSash3d(x))
|
||||
;(((s as SashShape).sashes ?? []) as SashShape[]).forEach(x => collectSash3d(x))
|
||||
}
|
||||
collectSash3d(shape)
|
||||
const cm = shape.cm ?? {}
|
||||
const gm = cm.g
|
||||
? new THREE.MeshPhysicalMaterial({
|
||||
@@ -271,16 +279,22 @@ function build(drawing: DrawingData) {
|
||||
const regionBBs: BBox[] = regions.map(r => bboxOf(regionPoints(r)))
|
||||
const used = new Set<number>()
|
||||
const panels: { rect: BBox; sash: (typeof sashes)[number]; ri: number }[] = []
|
||||
// 先统计各区域覆盖扇数: 多扇共享(推拉/对开门)时保留静态底玻璃, 独占区域才跳过
|
||||
const coverCount = new Map<number, number>()
|
||||
for (const s of sashes) {
|
||||
const sb: BBox = { x0: s.x0, y0: s.y0, x1: s.x1, y1: s.y1 }
|
||||
let best = -1, bestA = 0
|
||||
regionBBs.forEach((rb, ri) => {
|
||||
if (used.has(ri)) return
|
||||
const a = overlapArea(sb, rb)
|
||||
if (a > bestA) { bestA = a; best = ri }
|
||||
})
|
||||
if (best >= 0 && bestA > 0) { used.add(best); panels.push({ rect: regionBBs[best], sash: s, ri: best }) }
|
||||
if (best >= 0 && bestA > 0) {
|
||||
coverCount.set(best, (coverCount.get(best) ?? 0) + 1)
|
||||
// 扇面板用 glyph 自身矩形(多扇可共享同一区域)
|
||||
panels.push({ rect: sb, sash: s, ri: best })
|
||||
}
|
||||
}
|
||||
for (const [ri, n] of coverCount) if (n === 1) used.add(ri)
|
||||
|
||||
/** 装饰格条(自研 decs 扩展): 玻璃前表面加横竖格条体 */
|
||||
const addDecBars = (parent: THREE.Group, dec: DecBar, w: number, h: number, mx: number, my: number, z: number) => {
|
||||
@@ -333,6 +347,57 @@ function build(drawing: DrawingData) {
|
||||
barY(group, sashMat, h, t, DEPTH * 0.7, w / 2 - t / 2, 0, 0)
|
||||
const dec = (shape.decs ?? []).find(d => d.ri === ri)
|
||||
if (dec) addDecBars(group, dec, w, h, 0, 0, 0.011)
|
||||
// 扇内子格(源站原生): 裙板实板 + dbs 格条(锚点归属匹配扇形状: sp 锚点落在该扇矩形内)
|
||||
const gRect = { x0: s.x0, y0: s.y0, x1: s.x1, y1: s.y1 }
|
||||
const matched = sashShapes3d.find(sh => (sh.mm?.sp ?? []).some(sp => {
|
||||
const pt = sp.line?.pt
|
||||
return pt && pt.x >= gRect.x0 && pt.x <= gRect.x1 && pt.y >= gRect.y0 && pt.y <= gRect.y1
|
||||
})) ?? sashShapes3d[k % Math.max(1, sashShapes3d.length)]
|
||||
if (matched) {
|
||||
for (const p of inferSashPanels(matched, gRect)) {
|
||||
const pw = (p.x1 - p.x0) * S, ph = (p.y1 - p.y0) * S
|
||||
// 子格中心 → group 局部坐标(group 原点=扇矩形中心)
|
||||
const lmx = ((p.x0 + p.x1) / 2 - gRect.x1 / 2 - gRect.x0 / 2) * S
|
||||
const lmy = (gRect.y1 / 2 + gRect.y0 / 2 - (p.y0 + p.y1) / 2) * S
|
||||
if (![pw, ph, lmx, lmy].every(Number.isFinite) || pw <= 0.01 || ph <= 0.01) continue
|
||||
if (!p.glass) {
|
||||
const board = new THREE.Mesh(new THREE.BoxGeometry(pw, ph, 0.012),
|
||||
new THREE.MeshStandardMaterial({ color: 0xd9cdb8, roughness: 0.7, metalness: 0.05 }))
|
||||
board.position.set(lmx, lmy, 0.004)
|
||||
group.add(board)
|
||||
} else if (p.deco) {
|
||||
// hc 等分主格 + gp 内框 + 四角回纹块(3D 条体)
|
||||
const bw = Math.max(0.006, 0.014 * S)
|
||||
const inner = Math.max(0.015, p.deco.gp * S)
|
||||
const barMat = new THREE.MeshStandardMaterial({ color: 0x6b563c, roughness: 0.5, metalness: 0.25 })
|
||||
const iw = pw - 2 * inner, ih = ph - 2 * inner
|
||||
if (iw > 0.02 && ih > 0.02) {
|
||||
// 内圈框
|
||||
barX(group, barMat, iw, bw, DEPTH * 0.2, lmx, lmy + ih / 2, 0.008, 1)
|
||||
barX(group, barMat, iw, bw, DEPTH * 0.2, lmx, lmy - ih / 2, 0.008, 1)
|
||||
barY(group, barMat, ih, bw, DEPTH * 0.2, lmx - iw / 2, lmy, 0.008, 1)
|
||||
barY(group, barMat, ih, bw, DEPTH * 0.2, lmx + iw / 2, lmy, 0.008, 1)
|
||||
// 主格等分
|
||||
const hc = Math.max(2, p.deco.hc)
|
||||
for (let i = 1; i < hc; i++) {
|
||||
barY(group, barMat, ih, bw, DEPTH * 0.2, lmx - iw / 2 + (iw * i) / hc, lmy, 0.008, 1)
|
||||
barX(group, barMat, iw, bw, DEPTH * 0.2, lmx, lmy + ih / 2 - (ih * i) / hc, 0.008, 1)
|
||||
}
|
||||
// 四角回纹块(嵌套双框)
|
||||
const qs = Math.min(inner * 0.72, iw * 0.14, ih * 0.14)
|
||||
for (const [sx, sy] of [[-1, 1], [1, 1], [-1, -1], [1, -1]] as const) {
|
||||
const cx2 = lmx + sx * (iw / 2 - qs / 2), cy2 = lmy + sy * (ih / 2 - qs / 2)
|
||||
for (const f of [1, 0.55]) {
|
||||
barX(group, barMat, qs * f, bw, DEPTH * 0.2, cx2, cy2 + sy * (qs * f) / 2, 0.008, 1)
|
||||
barX(group, barMat, qs * f, bw, DEPTH * 0.2, cx2, cy2 - sy * (qs * f) / 2, 0.008, 1)
|
||||
barY(group, barMat, qs * f, bw, DEPTH * 0.2, cx2 + sx * (qs * f) / 2, cy2, 0.008, 1)
|
||||
barY(group, barMat, qs * f, bw, DEPTH * 0.2, cx2 - sx * (qs * f) / 2, cy2, 0.008, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
group.position.set(cxr, cyr, 0)
|
||||
sashRoot.add(group)
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface SashShape {
|
||||
openToward?: string // inward/outward
|
||||
rt?: number | boolean // 圆弧半径(弧顶扇)
|
||||
sashes?: SashEntry[] // DoubleSash 的子扇
|
||||
mm?: { sp?: SplitLine[]; dbs?: unknown; fls?: { fls?: any[] }; phs?: unknown[]; lpfs?: unknown[] } // 扇内分割/玻璃/格条(源站原生)
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@@ -480,3 +481,53 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: ReplayResult | Seg
|
||||
}
|
||||
return { regions, sashes: glyphs }
|
||||
}
|
||||
|
||||
/** 扇内子格(源站原生结构消费):扇自身 mm.sp 内部分割 + mm.fls 玻璃挂接 + mm.dbs.bars 源站格条。
|
||||
* 扇无基底 polygon(几何=挂接区域矩形),以 rect 构造临时 shape 复用 replayShape 全语义重放。 */
|
||||
export interface SashPanel {
|
||||
x0: number; y0: number; x1: number; y1: number
|
||||
glass: boolean // true=玻璃格(挂 fls 或无分割默认) / false=实板(门裙板等)
|
||||
glassText?: string // 玻璃编号(ser.text)
|
||||
glassSpec?: string // 玻璃配置(spet.sp)
|
||||
deco?: { tp: string; gp: number; hc: number } // 源站格条(dbs.bars: tp=风格, gp=回纹尺寸 mm, hc=主格等分数)
|
||||
}
|
||||
|
||||
export function inferSashPanels(sash: SashShape, rect: { x0: number; y0: number; x1: number; y1: number }): SashPanel[] {
|
||||
const mm = ((sash as Record<string, unknown>).mm ?? {}) as { sp?: SplitLine[]; fls?: { fls?: any[] }; dbs?: unknown }
|
||||
const sps = mm.sp ?? []
|
||||
const fls = mm.fls?.fls ?? []
|
||||
if (!sps.length) {
|
||||
const fl = fls[0]
|
||||
return [{ ...rect, glass: true, glassText: fl?.ser?.text, glassSpec: fl?.spet?.sp }]
|
||||
}
|
||||
const ring = [
|
||||
[{ ps: { x: rect.x0, y: rect.y0 }, pe: { x: rect.x1, y: rect.y0 } },
|
||||
{ ps: { x: rect.x1, y: rect.y0 }, pe: { x: rect.x1, y: rect.y1 } },
|
||||
{ ps: { x: rect.x1, y: rect.y1 }, pe: { x: rect.x0, y: rect.y1 } },
|
||||
{ ps: { x: rect.x0, y: rect.y1 }, pe: { x: rect.x0, y: rect.y0 } }],
|
||||
]
|
||||
const fake = { ...(sash as Record<string, unknown>), polygon: { type: 'polygon', polygon: ring, spLine: null, virtual: false }, mm } as unknown as FrameShape
|
||||
const rp = replayShape(fake)
|
||||
const decos: { pid: { idx: number; pos: number }; tp: string; gp: number; hc: number }[] = []
|
||||
const barsRaw = (mm.dbs && typeof mm.dbs === 'object' ? (mm.dbs as { bars?: unknown[] }).bars : (mm.dbs as unknown[])) ?? []
|
||||
for (const entry of barsRaw as unknown[]) {
|
||||
try {
|
||||
const pair = entry as [string, { db?: { tp?: string; gp?: number } } | undefined]
|
||||
const pid = typeof pair[0] === 'string' ? JSON.parse(pair[0]) : pair[0]
|
||||
const db = (pair[1]?.db ?? pair[1]) as { tp?: string; gp?: number; hc?: number } | undefined
|
||||
if (pid && Number.isInteger(pid.idx)) decos.push({ pid, tp: String(db?.tp ?? ''), gp: Number(db?.gp) || 80, hc: Number(db?.hc) || 2 })
|
||||
} catch { /* 坏数据忽略 */ }
|
||||
}
|
||||
return rp.regions.map((segs, i) => {
|
||||
const bb = bboxOf(regionPoints(segs))
|
||||
const ref = rp.leafRefs[i] ?? { idx: -1, pos: -1 }
|
||||
const fl = fls.find(f => { const p = f?.pid ?? {}; return p.idx === ref.idx && p.pos === ref.pos })
|
||||
?? (fls.length === 1 && (fls[0]?.pid?.idx ?? -1) < 0 ? fls[0] : undefined)
|
||||
const isPanel = String(fl?.type ?? '') === 'Panel'
|
||||
const deco = decos.find(d => d.pid.idx === ref.idx && d.pid.pos === ref.pos)
|
||||
return { x0: bb.x0, y0: bb.y0, x1: bb.x1, y1: bb.y1,
|
||||
glass: isPanel ? false : Boolean(fl) || fls.length === 0,
|
||||
glassText: fl?.ser?.text, glassSpec: fl?.spet?.sp,
|
||||
deco: deco ? { tp: deco.tp, gp: deco.gp, hc: deco.hc } : undefined }
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user