diff --git a/CHANGELOG b/CHANGELOG index 1cebce2..753d710 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # CHANGELOG +- [2026-09-23 06:30] FEAT: v2.15 图纸封面图一致性收尾——官方 PNG 剩余元素补齐。①分段尺寸标注: 按重放区域分割点生成蓝色分段(每格宽/高)+总尺寸双排标注(端部刻度, 左侧文字旋转), 替换原仅总宽高文本; ②玻璃配置第二行: fl.spet.sp(如 5+12+5)画于编号下方(704 无配置不画, 与官方一致); ③中梃与外框交点白色连接件方块; 修复 spIdx 遍历变量; 验证: 704 画布与官方封面图逐元素一致(分段 663/1137/393/1207+总 1800/1600), ui-shots 6 页无报错 (Files: frontend/src/components/WindoorCanvas.vue, shots/*) + - [2026-09-23 06:05] FEAT: v2.14 2D 图纸样式对齐源站设计图(官方 PNG 逐元素比对)。WindoorCanvas 绘制层重写: ①玻璃 teal 渐变(官方配色)+深色细描边; ②型材立体三段线(深轮廓+紫灰主体+白高光, 宽度=60mm 料宽映射限幅), 外框/中梃/扇料框统一; ③四角白色角码; ④开向符号对齐官方: 平开=虚线大三角(apex 按 hm.openDirection 指铰链侧, wtf 同样画三角), 推拉=水平滑行箭头(成对反向); ⑤把手(铰链对面侧白条)+锁点小方块; ⑥玻璃编号标签(frame/sash 的 mm.fls[].ser.text 经 polyId 树寻址定位到格中心); ⑦尺寸标注改官方蓝色带端部刻度; CombineCanvas 玻璃/型材配色同步; 验证: 设计图纸 704 画布与官方平面图逐元素一致, ui-shots 6 页无报错 (Files: frontend/src/components/WindoorCanvas.vue, frontend/src/components/CombineCanvas.vue, shots/*) - [2026-09-23 05:40] FEAT: v2.13 细节尺寸编辑——组合与样板的每一格可精确编辑。①WindoorCanvas: 点击玻璃格选中(蓝虚线高亮+格尺寸标注), HUD 输入格W/格H 自动平移该格边界中梃(优先右/下边界其次左/上, 共线同位多段成组移动+组内各段裁剪 bbox 内缩30mm 交集 clamp); 选中中梃可输入距左/上边缘位置精确移动; 中梃拖动改组移动(修复共线多段拖动错位); ②CombineView: 工具栏新增"编辑细节"对话框(复用 WindoorCanvas 编辑器 editable 模式), 保存后原位写回对应单元形状(__uid/__name 保留, rev 驱动 3D 预览重建); ③CombineCanvas: 中梃改按各自裁剪 bbox 绘制(原横梃画满宽的示意画法与真实分格不符); ④TemplatesView 编辑提示文案更新; 验证: 真实浏览器截图(格选中 663×1207→格H=800 横梃精确下移/梃位 900 移动/组合对话框编辑保存写回后主画布与 3D 同步), vue-tsc 0 错误 (Files: frontend/src/components/WindoorCanvas.vue, frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue, frontend/src/views/TemplatesView.vue, frontend/scripts/verify-detail.mjs, shots/detail-*.png) diff --git a/frontend/src/components/WindoorCanvas.vue b/frontend/src/components/WindoorCanvas.vue index 72281e1..6b3e3b3 100644 --- a/frontend/src/components/WindoorCanvas.vue +++ b/frontend/src/components/WindoorCanvas.vue @@ -338,6 +338,7 @@ function drawFrame( const regions = rp.regions.map(regionPoints).filter(pts => pts.length >= 3) // 型材视觉宽度: 60mm 料宽映射像素, 限幅 const lw = Math.max(3, Math.min(10, 60 * scale)) + const frameBb = regions.length ? bboxOf(regions.flat()) : null // 1) 玻璃格: teal 渐变(官方图纸配色) for (const pts of regions) { @@ -356,11 +357,33 @@ function drawFrame( ctx.stroke() } - // 2) 中梃(按目标区域裁剪, 型材三段线; 选中高亮) - rp.clips.forEach(({ sp, bb }, spIdx) => { + // 2) 中梃(按目标区域裁剪, 型材三段线; 选中高亮) + 与外框交点白块(官方连接件符号) + for (let spIdx = 0; spIdx < rp.clips.length; spIdx++) { + const { sp, bb } = rp.clips[spIdx] const isSel = selected.value?.shapeIdx === shapeIdx && selected.value.spIdx === spIdx drawBar(ctx, spAxis(sp), spPos(sp), bb, tx, ty, scale, isSel) - }) + } + if (frameBb) { + const fx0 = Math.round(frameBb.x0), fy0 = Math.round(frameBb.y0) + const fx1 = Math.round(frameBb.x1), fy1 = Math.round(frameBb.y1) + const dot = Math.max(3, lw * 0.5) + ctx.fillStyle = '#f2f2f4' + ctx.strokeStyle = STYLE.profileDark + ctx.lineWidth = 1 + for (const { sp, bb } of rp.clips) { + const axis = spAxis(sp), pos = spPos(sp) + const ends = axis === 'v' ? [[pos, bb.y0], [pos, bb.y1]] : [[bb.x0, pos], [bb.x1, pos]] + for (const [ex, ey] of ends) { + const onFrame = (Math.round(ex) === fx0 || Math.round(ex) === fx1) && ey >= fy0 - 2 && ey <= fy1 + 2 + const onFrameY = (Math.round(ey) === fy0 || Math.round(ey) === fy1) && ex >= fx0 - 2 && ex <= fx1 + 2 + if (!(onFrame || onFrameY)) continue + ctx.beginPath() + ctx.rect(tx(ex) - dot / 2, ty(ey) - dot / 2, dot, dot) + ctx.fill() + ctx.stroke() + } + } + } // 3) 外框型材 + 四角角码 if (regions.length) { @@ -446,27 +469,35 @@ function drawFrame( } }) - // 5) 玻璃编号: 固定格(frame.mm.fls) + 扇格(sash.mm.fls), 经 polyId 树寻址定位 + // 5) 玻璃编号: 固定格(frame.mm.fls) + 扇格(sash.mm.fls), 经 polyId 树寻址定位; 次行=玻璃配置(spet.sp) ctx.fillStyle = STYLE.label ctx.textAlign = 'center' ctx.textBaseline = 'middle' - const drawCellLabel = (text: string, bb: { x0: number; y0: number; x1: number; y1: number }) => { + const drawCellLabel = (text: string, bb: { x0: number; y0: number; x1: number; y1: number }, subText?: string) => { const fs = Math.max(10, Math.min(16, (bb.x1 - bb.x0) * scale * 0.09)) + const cx = tx((bb.x0 + bb.x1) / 2), cy = ty((bb.y0 + bb.y1) / 2) ctx.font = `bold ${fs}px sans-serif` - ctx.fillText(text, tx((bb.x0 + bb.x1) / 2), ty((bb.y0 + bb.y1) / 2)) + ctx.fillText(text, cx, subText ? cy - fs * 0.6 : cy) + if (subText) { + ctx.font = `${Math.max(9, fs * 0.78)}px sans-serif` + ctx.fillText(subText, cx, cy + fs * 0.7) + } } for (const sub of (shape.sm ?? []) as SashShape[]) { if (sub.enabled === false) continue const text = ((sub.mm as any)?.fls as any)?.fls?.[0]?.ser?.text if (!text || sub.polyId?.idx == null) continue const segs = rp.resolveRef?.(sub.polyId.idx, sub.polyId.pos) - if (segs) drawCellLabel(text, bboxOf(regionPoints(segs))) + if (segs) { + const subText = ((sub.mm as any)?.fls as any)?.fls?.[0]?.spet?.sp || undefined + drawCellLabel(text, bboxOf(regionPoints(segs)), subText) + } } for (const fl of ((shape.mm as any)?.fls as any)?.fls ?? []) { const text = fl?.ser?.text if (!text || fl.pid?.idx == null) continue const segs = rp.resolveRef?.(fl.pid.idx, fl.pid.pos) - if (segs) drawCellLabel(text, bboxOf(regionPoints(segs))) + if (segs) drawCellLabel(text, bboxOf(regionPoints(segs)), fl?.spet?.sp || undefined) } ctx.textBaseline = 'alphabetic' @@ -489,31 +520,71 @@ function drawFrame( } } - // 7) 尺寸标注(官方蓝色刻度线) - if (regions.length) { - const bb = bboxOf(regions.flat()) - const x0 = tx(bb.x0), x1 = tx(bb.x1), y0 = ty(bb.y0), y1 = ty(bb.y1) - const wmm = Math.round(bb.x1 - bb.x0), hmm = Math.round(bb.y1 - bb.y0) - ctx.fillStyle = STYLE.dim + // 7) 尺寸标注(官方蓝色): 分段(每格宽高) + 总尺寸 + if (frameBb) { + const x0 = tx(frameBb.x0), y0 = ty(frameBb.y0), x1 = tx(frameBb.x1), y1 = ty(frameBb.y1) + const xs = new Set(), ys = new Set() + for (const pts of regions) { + const b = bboxOf(pts) + xs.add(Math.round(b.x0)); xs.add(Math.round(b.x1)) + ys.add(Math.round(b.y0)); ys.add(Math.round(b.y1)) + } + const xsA = [...xs].sort((a, b) => a - b) + const ysA = [...ys].sort((a, b) => a - b) ctx.strokeStyle = STYLE.dim - ctx.font = '11px sans-serif' - ctx.textAlign = 'center' - ctx.textBaseline = 'alphabetic' - ctx.fillText(`${wmm} mm`, (x0 + x1) / 2, y1 + 16) - ctx.save() - ctx.translate(x0 - 14, (y0 + y1) / 2) - ctx.rotate(-Math.PI / 2) - ctx.fillText(`${hmm} mm`, 0, 0) - ctx.restore() + ctx.fillStyle = STYLE.dim ctx.lineWidth = 1 + ctx.font = '10px sans-serif' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + const tick = (x: number, y: number, vertical: boolean) => { + ctx.beginPath() + if (vertical) { ctx.moveTo(x - 3, y); ctx.lineTo(x + 3, y) } + else { ctx.moveTo(x, y - 3); ctx.lineTo(x, y + 3) } + ctx.stroke() + } + // 底部: 分段宽 + 总宽 + const segY = y1 + 14 ctx.beginPath() - ctx.moveTo(x0, y1 + 6); ctx.lineTo(x1, y1 + 6) - ctx.moveTo(x0, y1 + 3); ctx.lineTo(x0, y1 + 9) - ctx.moveTo(x1, y1 + 3); ctx.lineTo(x1, y1 + 9) - ctx.moveTo(x0 - 6, y0); ctx.lineTo(x0 - 6, y1) - ctx.moveTo(x0 - 9, y0); ctx.lineTo(x0 - 3, y0) - ctx.moveTo(x0 - 9, y1); ctx.lineTo(x0 - 3, y1) + for (let i = 0; i < xsA.length - 1; i++) { + const a = tx(xsA[i]), b = tx(xsA[i + 1]) + if (b - a < 16) continue + ctx.moveTo(a, segY); ctx.lineTo(b, segY) + tick(a, segY, false); tick(b, segY, false) + ctx.fillText(String(xsA[i + 1] - xsA[i]), (a + b) / 2, segY - 8) + } ctx.stroke() + const dimY = segY + (xsA.length > 2 ? 14 : 0) + ctx.beginPath(); ctx.moveTo(x0, dimY); ctx.lineTo(x1, dimY) + tick(x0, dimY, false); tick(x1, dimY, false) + ctx.stroke() + ctx.fillText(`${Math.round(frameBb.x1 - frameBb.x0)}`, (x0 + x1) / 2, dimY - 8) + // 左侧: 分段高 + 总高(文字旋转) + const segX = x0 - 14 + ctx.beginPath() + for (let i = 0; i < ysA.length - 1; i++) { + const a = ty(ysA[i]), b = ty(ysA[i + 1]) + if (b - a < 16) continue + ctx.moveTo(segX, a); ctx.lineTo(segX, b) + tick(segX, a, true); tick(segX, b, true) + ctx.save() + ctx.translate(segX - 8, (a + b) / 2) + ctx.rotate(-Math.PI / 2) + ctx.fillText(String(ysA[i + 1] - ysA[i]), 0, 0) + ctx.restore() + } + ctx.stroke() + const dimX = segX - (ysA.length > 2 ? 14 : 0) + ctx.beginPath(); ctx.moveTo(dimX, y0); ctx.lineTo(dimX, y1) + tick(dimX, y0, true); tick(dimX, y1, true) + ctx.stroke() + ctx.save() + ctx.translate(dimX - 8, (y0 + y1) / 2) + ctx.rotate(-Math.PI / 2) + ctx.fillText(`${Math.round(frameBb.y1 - frameBb.y0)}`, 0, 0) + ctx.restore() + ctx.textAlign = 'left' + ctx.textBaseline = 'alphabetic' } } diff --git a/shots/02-combine-two.png b/shots/02-combine-two.png index c7e982b..77caa25 100644 Binary files a/shots/02-combine-two.png and b/shots/02-combine-two.png differ diff --git a/shots/04-combine-3d.png b/shots/04-combine-3d.png index 71b5d85..17646b1 100644 Binary files a/shots/04-combine-3d.png and b/shots/04-combine-3d.png differ diff --git a/shots/verify-2d-canvas.png b/shots/verify-2d-canvas.png index 586c4d5..3cf1b31 100644 Binary files a/shots/verify-2d-canvas.png and b/shots/verify-2d-canvas.png differ