下载 PNG
@@ -301,43 +335,152 @@ async function generateCloud(mode: 'quick' | 'photo' | 'blueprint', sketch_b64?:
}
}
-/** 工程大样图: 底图优先上传的手绘草图照片, 否则 2D 图纸画布截图; AI 重绘为黑白 CAD 大样图 */
-const sketchPhoto = ref('')
+/** 工程大样图: 草图上传→旋转转正→框选裁剪(拆窗)→生成; 无草图时用 2D 图纸画布截图 */
+const sketchImg = ref(null)
+const bpSketchRef = ref()
+const bpRot = ref(0) // 旋转角 0/90/180/270
+const bpRect = ref<{ x: number; y: number; w: number; h: number } | null>(null) // 框选(显示坐标)
+const bpCrop = ref('') // 裁剪结果 dataURL
+const lastBpBase = ref('') // 本次生成底图(结果对比展示)
+let bpDragStart: { x: number; y: number } | null = null
+let bpRotCanvas: HTMLCanvasElement | null = null // 旋转后整图离屏(≤1536)
function onSketchUpload(e: { file: unknown }) {
// el-upload http-request: options.file 即原始 File(部分版本包 raw)
const opt = e.file as { raw?: File } | File
const file = (opt as { raw?: File }).raw ?? (opt as File)
if (!(file instanceof File)) return
- const reader = new FileReader()
- reader.onload = () => {
- // 手机原图过大(5~10MB)会导致生图中转超时/拒绝, 统一压至 ≤1536px JPEG
- const img = new Image()
- img.onload = () => {
- const scale = Math.min(1, 1536 / Math.max(img.naturalWidth, img.naturalHeight))
- const cv = document.createElement('canvas')
- cv.width = Math.max(1, Math.round(img.naturalWidth * scale))
- cv.height = Math.max(1, Math.round(img.naturalHeight * scale))
- cv.getContext('2d')!.drawImage(img, 0, 0, cv.width, cv.height)
- sketchPhoto.value = cv.toDataURL('image/jpeg', 0.9)
- ElMessage.success('草图已上传')
- }
- img.onerror = () => ElMessage.error('图片读取失败,请重试')
- img.src = String(reader.result)
+ const url = URL.createObjectURL(file)
+ const img = new Image()
+ img.onload = async () => {
+ sketchImg.value = img
+ bpRot.value = 0
+ bpRect.value = null
+ bpCrop.value = ''
+ URL.revokeObjectURL(url)
+ await nextTick()
+ buildBpRotCanvas()
+ drawBpStage()
+ ElMessage.success('草图已上传,可旋转转正或框选裁剪后生成')
}
- reader.readAsDataURL(file)
+ img.onerror = () => ElMessage.error('图片读取失败,请重试')
+ img.src = url
}
-async function generateBlueprint() {
- // 上传的手绘草图优先(脱离样板上下文); 无上传时用 2D 图纸画布截图
- if (sketchPhoto.value) {
- await generateCloud('blueprint', sketchPhoto.value.split(',')[1], true)
- return
+function removeSketch() {
+ sketchImg.value = null
+ bpRot.value = 0
+ bpRect.value = null
+ bpCrop.value = ''
+ bpRotCanvas = null
+}
+
+function rotBp(deg: number) {
+ bpRot.value = (bpRot.value + deg + 360) % 360
+ bpRect.value = null
+ buildBpRotCanvas()
+ drawBpStage()
+}
+
+function clearBpCrop() {
+ bpRect.value = null
+ bpCrop.value = ''
+ drawBpStage()
+}
+
+/** 旋转后整图离屏 canvas(≤1536px):裁剪源 + 生成底图 */
+function buildBpRotCanvas() {
+ const img = sketchImg.value
+ if (!img) return
+ const rotated = bpRot.value % 180 !== 0
+ const srcW = rotated ? img.naturalHeight : img.naturalWidth
+ const srcH = rotated ? img.naturalWidth : img.naturalHeight
+ const scale = Math.min(1, 1536 / Math.max(srcW, srcH))
+ const cv = bpRotCanvas ?? document.createElement('canvas')
+ cv.width = Math.max(1, Math.round(srcW * scale))
+ cv.height = Math.max(1, Math.round(srcH * scale))
+ const ctx = cv.getContext('2d')!
+ ctx.save()
+ ctx.translate(cv.width / 2, cv.height / 2)
+ ctx.rotate((bpRot.value * Math.PI) / 180)
+ ctx.drawImage(img, (-img.naturalWidth * scale) / 2, (-img.naturalHeight * scale) / 2,
+ img.naturalWidth * scale, img.naturalHeight * scale)
+ ctx.restore()
+ bpRotCanvas = cv
+}
+
+/** 草图舞台: 旋转后整图 + 框选遮罩(bpRect 为显示坐标) */
+function drawBpStage() {
+ const cv = bpSketchRef.value
+ const base = bpRotCanvas
+ if (!cv || !base) return
+ const fit = Math.min(1, Math.min(920, window.innerWidth - 140) / base.width)
+ cv.width = Math.round(base.width * fit)
+ cv.height = Math.round(base.height * fit)
+ const ctx = cv.getContext('2d')!
+ ctx.drawImage(base, 0, 0, cv.width, cv.height)
+ if (bpRect.value) {
+ const f = base.width / cv.width
+ const { x, y, w, h } = bpRect.value
+ ctx.save()
+ ctx.fillStyle = 'rgba(0,0,0,0.35)'
+ ctx.fillRect(0, 0, cv.width, cv.height)
+ ctx.drawImage(base, x * f, y * f, w * f, h * f, x, y, w, h)
+ ctx.strokeStyle = '#5b3fd4'
+ ctx.lineWidth = 2.5
+ ctx.setLineDash([8, 5])
+ ctx.strokeRect(x, y, w, h)
+ ctx.restore()
}
- await nextTick()
- const dataUrl = bpCanvasRef.value?.exportImage?.()
- if (!dataUrl) { ElMessage.warning('图纸截图失败,请稍后重试'); return }
- await generateCloud('blueprint', dataUrl.split(',')[1])
+}
+
+function onBpDown(e: MouseEvent) {
+ const cv = bpSketchRef.value
+ if (!cv) return
+ const b = cv.getBoundingClientRect()
+ bpDragStart = { x: e.clientX - b.left, y: e.clientY - b.top }
+ bpRect.value = null
+}
+function onBpMove(e: MouseEvent) {
+ if (!bpDragStart || !bpSketchRef.value) return
+ const b = bpSketchRef.value.getBoundingClientRect()
+ const p = { x: e.clientX - b.left, y: e.clientY - b.top }
+ bpRect.value = { x: Math.min(bpDragStart.x, p.x), y: Math.min(bpDragStart.y, p.y),
+ w: Math.abs(p.x - bpDragStart.x), h: Math.abs(p.y - bpDragStart.y) }
+ drawBpStage()
+}
+function onBpUp() {
+ if (bpRect.value && (bpRect.value.w < 25 || bpRect.value.h < 25)) bpRect.value = null
+ bpDragStart = null
+ drawBpStage()
+}
+
+async function generateBlueprint(regen = false) {
+ let base = ''
+ if (sketchImg.value) {
+ if (bpRect.value && bpRotCanvas && bpSketchRef.value) {
+ // 框选裁剪: 显示坐标 → 离屏图坐标
+ const f = bpRotCanvas.width / bpSketchRef.value.width
+ const r = bpRect.value
+ const c = document.createElement('canvas')
+ c.width = Math.max(1, Math.round(r.w * f))
+ c.height = Math.max(1, Math.round(r.h * f))
+ c.getContext('2d')!.drawImage(bpRotCanvas, r.x * f, r.y * f, r.w * f, r.h * f, 0, 0, c.width, c.height)
+ base = c.toDataURL('image/jpeg', 0.9)
+ bpCrop.value = base
+ } else {
+ buildBpRotCanvas()
+ base = bpRotCanvas!.toDataURL('image/jpeg', 0.9)
+ }
+ } else {
+ await nextTick()
+ const dataUrl = bpCanvasRef.value?.exportImage?.()
+ if (!dataUrl) { ElMessage.warning('图纸截图失败,请稍后重试'); return }
+ base = dataUrl
+ }
+ lastBpBase.value = base
+ void regen
+ await generateCloud('blueprint', base.split(',')[1], !!sketchImg.value)
}
/** 照片模式参考图: 全图压至 ≤1536px JPEG base64 */
@@ -438,8 +581,12 @@ onMounted(async () => {
background: rgba(0,0,0,0.5); padding: 4px 10px; border-radius: 10px; pointer-events: none; }
.preview { height: 58vh; }
.bp-stage { height: 58vh; border: 1px solid #e4e7ed; border-radius: 6px; overflow: hidden; }
-.bp-photo { display: flex; align-items: center; justify-content: center; background: #f5f6f8; }
-.sketch-img { max-width: 100%; max-height: 100%; object-fit: contain; }
+.bp-edit { position: relative; display: flex; justify-content: center; align-items: flex-start; background: #f5f6f8; }
+.bp-cv { max-width: 100%; max-height: 100%; cursor: crosshair; }
+.bp-compare { display: flex; gap: 12px; align-items: stretch; }
+.bp-compare .cmp { flex: 1; min-width: 0; border: 1px solid #e4e7ed; border-radius: 8px; overflow: hidden; background: #fafbfc; }
+.bp-compare .cap { padding: 6px 10px; font-size: 12px; font-weight: 600; color: #606266; background: #f2f3f5; }
+.cmp-img { display: block; width: 100%; height: auto; }
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; margin-top: 14px; }
.gallery-foot { margin-top: 8px; }
.ai-result { margin-top: 14px; text-align: center; }
diff --git a/shots/bp-single-out.png b/shots/bp-single-out.png
new file mode 100644
index 0000000..502d91f
Binary files /dev/null and b/shots/bp-single-out.png differ
diff --git a/shots/bp-single-src.jpg b/shots/bp-single-src.jpg
new file mode 100644
index 0000000..3c8d1f9
Binary files /dev/null and b/shots/bp-single-src.jpg differ
diff --git a/shots/opt-1-upload.png b/shots/opt-1-upload.png
new file mode 100644
index 0000000..71833d5
Binary files /dev/null and b/shots/opt-1-upload.png differ
diff --git a/shots/opt-2-rot.png b/shots/opt-2-rot.png
new file mode 100644
index 0000000..2b2ad2e
Binary files /dev/null and b/shots/opt-2-rot.png differ
diff --git a/shots/opt-3-crop.png b/shots/opt-3-crop.png
new file mode 100644
index 0000000..a0cd2ea
Binary files /dev/null and b/shots/opt-3-crop.png differ
diff --git a/shots/opt-4-result.png b/shots/opt-4-result.png
new file mode 100644
index 0000000..62898c0
Binary files /dev/null and b/shots/opt-4-result.png differ