feat: P0-7 玻璃装饰格条设计器(8风格/2D+3D渲染/算料计价)
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
controls-position="right" style="width:92px" @change="onRegionSize" /></span>
|
||||
<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>
|
||||
<el-button size="small" type="warning" plain @click="openDec">格条</el-button>
|
||||
</template>
|
||||
<template v-else-if="selected">
|
||||
<span class="dim-input">梃位 <el-input-number v-model="mulPos" :min="0" :step="10" size="small"
|
||||
@@ -26,6 +27,34 @@
|
||||
</template>
|
||||
<el-button size="small" text @click="reset">重置视图</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 装饰格条设计对话框 -->
|
||||
<el-dialog v-model="decVisible" title="装饰格条设计" width="520px" append-to-body>
|
||||
<div class="dec-presets">
|
||||
<div v-for="p in DEC_PRESETS" :key="p.kind" class="dec-preset" :class="{ active: decForm.kind === p.kind }"
|
||||
@click="applyPreset(p)">
|
||||
<div class="dec-thumb" :style="thumbStyle(p)">
|
||||
<div class="dec-thumb-bar" :style="{ background: p.color }" />
|
||||
</div>
|
||||
<span>{{ p.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-form label-width="80px" style="margin-top:10px">
|
||||
<el-form-item label="横条数"><el-input-number v-model="decForm.rows" :min="0" :max="8" size="small" /></el-form-item>
|
||||
<el-form-item label="竖条数"><el-input-number v-model="decForm.cols" :min="0" :max="8" size="small" /></el-form-item>
|
||||
<el-form-item label="条宽 mm"><el-input-number v-model="decForm.bw" :min="8" :max="60" size="small" /></el-form-item>
|
||||
<el-form-item label="颜色">
|
||||
<el-radio-group v-model="decForm.color" size="small">
|
||||
<el-radio-button v-for="c in DEC_COLORS" :key="c.v" :value="c.v">{{ c.label }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button v-if="hasDec" type="warning" plain @click="removeDec">清除该格格条</el-button>
|
||||
<el-button @click="decVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="applyDec">应用</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -44,11 +73,11 @@
|
||||
* - 改梃: 直接点击中梃选中(高亮) → 拖动移动(line.pt 更新后重放) → 删除梃或 Del 键删除
|
||||
* - 撤销栈; exportDrawing() 取回 drawing_data; change 事件通知父组件
|
||||
*/
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
bboxOf, inferSashGlyphs, parseDrawing, regionPoints, replayShape, spAxis, spPos,
|
||||
type DrawingData, type FrameShape, type Pt, type Segment, type SashShape, type SplitLine,
|
||||
bboxOf, DEC_PRESETS, inferSashGlyphs, parseDrawing, regionPoints, replayShape, spAxis, spPos,
|
||||
type DecBar, type DrawingData, type FrameShape, type Pt, type Segment, type SashShape, type SplitLine,
|
||||
} from '../types/drawing'
|
||||
|
||||
const props = defineProps<{ drawingData: string | object; editable?: boolean }>()
|
||||
@@ -378,6 +407,35 @@ function drawFrame(
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 1.5) 装饰格条(自研 decs 扩展, 挂在玻璃格上; ri 对齐重放区域索引)
|
||||
for (const dec of (shape.decs ?? []) as DecBar[]) {
|
||||
const segs = rp.regions[dec.ri]
|
||||
if (!segs) continue
|
||||
const pts = regionPoints(segs)
|
||||
if (pts.length < 3) continue
|
||||
const bb = bboxOf(pts)
|
||||
const px0 = tx(bb.x0), py0 = ty(bb.y0), px1 = tx(bb.x1), py1 = ty(bb.y1)
|
||||
const gw = px1 - px0, gh = py1 - py0
|
||||
if (gw < 12 || gh < 12) continue
|
||||
const barPx = Math.max(1.5, dec.bw * scale)
|
||||
ctx.save()
|
||||
ctx.beginPath(); ctx.rect(px0, py0, gw, gh); ctx.clip()
|
||||
ctx.fillStyle = dec.color
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.4)'
|
||||
ctx.lineWidth = 1
|
||||
for (let i = 1; i <= dec.rows; i++) {
|
||||
const y = py0 + (gh * i) / (dec.rows + 1)
|
||||
ctx.fillRect(px0, y - barPx / 2, gw, barPx)
|
||||
ctx.strokeRect(px0, y - barPx / 2, gw, barPx)
|
||||
}
|
||||
for (let i = 1; i <= dec.cols; i++) {
|
||||
const x = px0 + (gw * i) / (dec.cols + 1)
|
||||
ctx.fillRect(x - barPx / 2, py0, barPx, gh)
|
||||
ctx.strokeRect(x - barPx / 2, py0, barPx, gh)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 2) 中梃(按目标区域裁剪, 型材三段线; 选中高亮) + 与外框交点白块(官方连接件符号)
|
||||
for (let spIdx = 0; spIdx < rp.clips.length; spIdx++) {
|
||||
const { sp, bb } = rp.clips[spIdx]
|
||||
@@ -796,6 +854,71 @@ function undo() {
|
||||
function exportDrawing(): string | null {
|
||||
return dd ? JSON.stringify(dd) : null
|
||||
}
|
||||
|
||||
// ---- 装饰格条设计(P0-格条设计器) ----
|
||||
const DEC_COLORS = [
|
||||
{ label: '黑', v: '#2b2b2b' }, { label: '白', v: '#ffffff' }, { label: '灰', v: '#9aa0a6' },
|
||||
{ label: '金', v: '#c9a86a' }, { label: '棕', v: '#6b4a2b' }, { label: '蓝', v: '#3a6ea5' },
|
||||
]
|
||||
const decVisible = ref(false)
|
||||
const decForm = reactive<DecBar>({ ri: 0, kind: 'modern', rows: 1, cols: 1, bw: 25, color: '#2b2b2b' })
|
||||
const hasDec = ref(false)
|
||||
|
||||
function openDec() {
|
||||
if (!selRegion.value || !dd) return
|
||||
const { shapeIdx, ri } = selRegion.value
|
||||
const shape = dd.sm[shapeIdx]
|
||||
const existing = (shape?.decs ?? []).find(d => d.ri === ri)
|
||||
hasDec.value = !!existing
|
||||
Object.assign(decForm, existing ?? { ri, kind: 'modern', rows: 1, cols: 1, bw: 25, color: '#2b2b2b' }, { ri })
|
||||
decVisible.value = true
|
||||
}
|
||||
|
||||
function applyPreset(p: (typeof DEC_PRESETS)[number]) {
|
||||
Object.assign(decForm, { kind: p.kind, rows: p.rows, cols: p.cols, bw: p.bw, color: p.color })
|
||||
}
|
||||
|
||||
function thumbStyle(p: (typeof DEC_PRESETS)[number]) {
|
||||
// 纯 CSS 缩略图: 用线性渐变模拟格条
|
||||
const rowPct = p.rows ? 100 / (p.rows + 1) : 0
|
||||
const colPct = p.cols ? 100 / (p.cols + 1) : 0
|
||||
const horizontal = Array.from({ length: p.rows }, (_, i) => `${p.color} ${rowPct * (i + 1) - 2}% ${rowPct * (i + 1) + 2}%`).join(',')
|
||||
const vertical = Array.from({ length: p.cols }, (_, i) => `${p.color} ${colPct * (i + 1) - 2}% ${colPct * (i + 1) + 2}%`).join(',')
|
||||
const base = 'linear-gradient(135deg,#dceef0,#bcd9dc)'
|
||||
const grad = [base, ...(horizontal ? [`linear-gradient(0deg,transparent,${horizontal},transparent)`] : []),
|
||||
...(vertical ? [`linear-gradient(90deg,transparent,${vertical},transparent)`] : [])]
|
||||
return { background: grad.join(',') }
|
||||
}
|
||||
|
||||
function applyDec() {
|
||||
if (!selRegion.value || !dd) return
|
||||
const { shapeIdx } = selRegion.value
|
||||
const shape = dd.sm[shapeIdx]
|
||||
if (!shape) return
|
||||
undoStack.push(JSON.stringify(dd))
|
||||
shape.decs = shape.decs ?? []
|
||||
const found = shape.decs.find(d => d.ri === decForm.ri)
|
||||
if (found) Object.assign(found, JSON.parse(JSON.stringify(decForm)))
|
||||
else shape.decs.push(JSON.parse(JSON.stringify(decForm)))
|
||||
decVisible.value = false
|
||||
render()
|
||||
emit('change')
|
||||
ElMessage.success('格条已应用')
|
||||
}
|
||||
|
||||
function removeDec() {
|
||||
if (!selRegion.value || !dd) return
|
||||
const { shapeIdx, ri } = selRegion.value
|
||||
const shape = dd.sm[shapeIdx]
|
||||
if (!shape?.decs) return
|
||||
undoStack.push(JSON.stringify(dd))
|
||||
shape.decs = shape.decs.filter(d => d.ri !== ri)
|
||||
hasDec.value = false
|
||||
decVisible.value = false
|
||||
render()
|
||||
emit('change')
|
||||
ElMessage.success('格条已清除')
|
||||
}
|
||||
defineExpose({ exportDrawing })
|
||||
|
||||
// ============ 视图控制 ============
|
||||
@@ -958,4 +1081,11 @@ canvas:active { cursor: grabbing; }
|
||||
.hud { position: absolute; right: 10px; top: 10px; display: flex; gap: 8px; align-items: center;
|
||||
background: rgba(255,255,255,0.92); border-radius: 6px; padding: 2px 8px; font-size: 12px; color: #606266; flex-wrap: wrap; }
|
||||
.dim-input { display: inline-flex; align-items: center; gap: 4px; color: #409eff; font-weight: 600; }
|
||||
.dec-presets { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
|
||||
.dec-preset { display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer;
|
||||
padding: 6px; border: 1.5px solid transparent; border-radius: 8px; font-size: 12px; color: #606266; }
|
||||
.dec-preset:hover { background: #f5f7fa; }
|
||||
.dec-preset.active { border-color: #409eff; background: #ecf5ff; }
|
||||
.dec-thumb { width: 100%; height: 44px; border: 1px solid #dcdfe6; border-radius: 4px; overflow: hidden; }
|
||||
.dec-thumb-bar { width: 100%; height: 100%; }
|
||||
</style>
|
||||
|
||||
@@ -58,7 +58,7 @@ import { ElMessage } from 'element-plus'
|
||||
import * as THREE from 'three'
|
||||
import {
|
||||
bboxOf, inferSashGlyphs, parseDrawing, regionPoints, replayShape,
|
||||
type DrawingData, type Pt,
|
||||
type DecBar, type DrawingData, type Pt,
|
||||
} from '../types/drawing'
|
||||
import { profileGeo } from '../three/profileGeo'
|
||||
|
||||
@@ -268,7 +268,7 @@ 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] }[] = []
|
||||
const panels: { rect: BBox; sash: (typeof sashes)[number]; ri: 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
|
||||
@@ -277,7 +277,23 @@ function build(drawing: DrawingData) {
|
||||
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 }) }
|
||||
if (best >= 0 && bestA > 0) { used.add(best); panels.push({ rect: regionBBs[best], sash: s, ri: best }) }
|
||||
}
|
||||
|
||||
/** 装饰格条(自研 decs 扩展): 玻璃前表面加横竖格条体 */
|
||||
const addDecBars = (parent: THREE.Group, dec: DecBar, w: number, h: number, mx: number, my: number, z: number) => {
|
||||
const bw = Math.max(0.004, dec.bw * S)
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(dec.color), roughness: 0.5, metalness: 0.3,
|
||||
})
|
||||
for (let i = 1; i <= dec.rows; i++) {
|
||||
const y = my - h / 2 + (h * i) / (dec.rows + 1)
|
||||
barX(parent, mat, w * 0.995, bw, DEPTH * 0.25, mx, y, z, 1)
|
||||
}
|
||||
for (let i = 1; i <= dec.cols; i++) {
|
||||
const x = mx - w / 2 + (w * i) / (dec.cols + 1)
|
||||
barY(parent, mat, h * 0.995, bw, DEPTH * 0.25, x, my, z, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 静态玻璃 + 中梃(区域薄型材)
|
||||
@@ -295,10 +311,12 @@ function build(drawing: DrawingData) {
|
||||
barX(frameGroup, mullionMat, w, t, DEPTH * 0.55, mx, my - h / 2 + t / 2, 0)
|
||||
barY(frameGroup, mullionMat, h, t, DEPTH * 0.55, mx - w / 2 + t / 2, my, 0)
|
||||
barY(frameGroup, mullionMat, h, t, DEPTH * 0.55, mx + w / 2 - t / 2, my, 0)
|
||||
const dec = (shape.decs ?? []).find(d => d.ri === ri)
|
||||
if (dec) addDecBars(frameGroup, dec, w, h, mx, my, 0.012)
|
||||
})
|
||||
|
||||
// 扇面板(可开合)
|
||||
panels.forEach(({ rect, sash: s }, k) => {
|
||||
panels.forEach(({ rect, sash: s, ri }, k) => {
|
||||
const w = (rect.x1 - rect.x0) * S, h = (rect.y1 - rect.y0) * S
|
||||
const cxr = ((rect.x0 + rect.x1) / 2 - cx) * S
|
||||
const cyr = (cy - (rect.y0 + rect.y1) / 2) * S
|
||||
@@ -311,6 +329,8 @@ function build(drawing: DrawingData) {
|
||||
barX(group, sashMat, w, t, DEPTH * 0.7, 0, -h / 2 + t / 2, 0)
|
||||
barY(group, sashMat, h, t, DEPTH * 0.7, -w / 2 + t / 2, 0, 0)
|
||||
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)
|
||||
group.position.set(cxr, cyr, 0)
|
||||
sashRoot.add(group)
|
||||
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
// src/types/drawing.ts
|
||||
var DEC_PRESETS = [
|
||||
{ kind: "modern", label: "\u73B0\u4EE3\u8F7B\u5962", rows: 1, cols: 1, bw: 25, color: "#2b2b2b" },
|
||||
{ kind: "italian", label: "\u610F\u5F0F\u7B80\u7EA6", rows: 1, cols: 1, bw: 18, color: "#9aa0a6" },
|
||||
{ kind: "cream", label: "\u5976\u6CB9\u98CE", rows: 2, cols: 2, bw: 22, color: "#f3ece2" },
|
||||
{ kind: "chinese", label: "\u4E2D\u5F0F", rows: 2, cols: 2, bw: 28, color: "#6b4a2b" },
|
||||
{ kind: "european", label: "\u6B27\u5F0F\u53E4\u5178", rows: 3, cols: 3, bw: 20, color: "#c9a86a" },
|
||||
{ kind: "french", label: "\u6CD5\u5F0F", rows: 2, cols: 2, bw: 16, color: "#d8c9a3" },
|
||||
{ kind: "medit", label: "\u5730\u4E2D\u6D77", rows: 2, cols: 3, bw: 24, color: "#3a6ea5" },
|
||||
{ kind: "pastoral", label: "\u7530\u56ED\u5BC6\u683C", rows: 4, cols: 4, bw: 14, color: "#ffffff" }
|
||||
];
|
||||
function parseDrawing(raw) {
|
||||
const dd = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||
return dd;
|
||||
}
|
||||
function regionPoints(region) {
|
||||
return region.map((seg) => ({ x: seg.ps.x, y: seg.ps.y }));
|
||||
}
|
||||
function bboxOf(points) {
|
||||
const xs = points.map((p) => p.x);
|
||||
const ys = points.map((p) => p.y);
|
||||
return { x0: Math.min(...xs), y0: Math.min(...ys), x1: Math.max(...xs), y1: Math.max(...ys) };
|
||||
}
|
||||
function spAxis(sp) {
|
||||
const n = sp.line?.norm ?? { x: 1, y: 0 };
|
||||
return Math.abs(n.x) >= Math.abs(n.y) ? "v" : "h";
|
||||
}
|
||||
function spPos(sp) {
|
||||
return spAxis(sp) === "v" ? sp.line.pt.x : sp.line.pt.y;
|
||||
}
|
||||
function pointInRegion(p, region) {
|
||||
let inside = false;
|
||||
const pts = regionPoints(region);
|
||||
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
||||
const xi = pts[i].x, yi = pts[i].y, xj = pts[j].x, yj = pts[j].y;
|
||||
if (yi > p.y !== yj > p.y && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
function dfsLeaves(node, out) {
|
||||
if (node.kids.length === 0) {
|
||||
out.push(node);
|
||||
return;
|
||||
}
|
||||
node.kids.forEach((k) => dfsLeaves(k, out));
|
||||
}
|
||||
function replayShape(shape) {
|
||||
const base = (shape.polygon?.polygon ?? []).map((r) => r.filter((s) => s && typeof s === "object" && s.ps && s.pe && Number.isFinite(s.ps.x) && Number.isFinite(s.ps.y) && Number.isFinite(s.pe.x) && Number.isFinite(s.pe.y)).map((s) => ({ ps: { ...s.ps }, pe: { ...s.pe } }))).filter((r) => r.length >= 3);
|
||||
const root = { segs: base[0] ?? [], kids: [] };
|
||||
const extraRoots = base.slice(1).map((segs) => ({ segs, kids: [] }));
|
||||
const idMap = /* @__PURE__ */ new Map();
|
||||
const clips = [];
|
||||
const allLeaves = () => {
|
||||
const out = [];
|
||||
dfsLeaves(root, out);
|
||||
extraRoots.forEach((r) => dfsLeaves(r, out));
|
||||
return out;
|
||||
};
|
||||
let spIdx = 0;
|
||||
for (const sp of shape.mm?.sp ?? []) {
|
||||
const myId = spIdx;
|
||||
spIdx++;
|
||||
if (!sp?.line?.pt || !sp.line.norm) continue;
|
||||
const axis = spAxis(sp);
|
||||
const pos = spPos(sp);
|
||||
let target = null;
|
||||
const nid = sp.polyId?.idx;
|
||||
const cpos = sp.polyId?.pos;
|
||||
if (Number.isInteger(nid) && nid >= 0) {
|
||||
const node = nid === 0 ? root : idMap.get(nid);
|
||||
if (node) {
|
||||
if (node.kids.length === 0) target = node;
|
||||
else if (Number.isInteger(cpos) && cpos >= 0 && cpos < node.kids.length) {
|
||||
target = node.kids[cpos];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!target) target = allLeaves().find((nd) => pointInRegion(sp.line.pt, nd.segs)) ?? null;
|
||||
if (!target || target.segs.length < 3) continue;
|
||||
const bb = bboxOf(regionPoints(target.segs));
|
||||
const lo = axis === "v" ? bb.x0 : bb.y0;
|
||||
const hi = axis === "v" ? bb.x1 : bb.y1;
|
||||
if (pos <= lo + 0.5 || pos >= hi - 0.5) {
|
||||
clips.push({ sp, bb });
|
||||
idMap.set(myId, target);
|
||||
continue;
|
||||
}
|
||||
const split = splitPolySegments(target.segs, axis, pos);
|
||||
if (!split) {
|
||||
clips.push({ sp, bb });
|
||||
idMap.set(myId, target);
|
||||
continue;
|
||||
}
|
||||
const nrm = sp.line.norm;
|
||||
const sideOf = (segs) => {
|
||||
for (const s of segs) {
|
||||
for (const p of [s.ps, s.pe]) {
|
||||
const d = (p.x - sp.line.pt.x) * nrm.x + (p.y - sp.line.pt.y) * nrm.y;
|
||||
if (Math.abs(d) > 1e-6) return d > 0;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const c0 = sideOf(split.a) ? split.a : split.b;
|
||||
target.kids = [{ segs: c0, kids: [] }, { segs: sideOf(split.a) ? split.b : split.a, kids: [] }];
|
||||
target.segs = [];
|
||||
idMap.set(myId, target);
|
||||
clips.push({ sp, bb });
|
||||
}
|
||||
const leaves = allLeaves();
|
||||
const regions = leaves.map((l) => l.segs).filter((s) => s.length >= 3);
|
||||
const nodeId = /* @__PURE__ */ new Map([[root, 0]]);
|
||||
const leafRefs = [];
|
||||
const walk = (node, id) => {
|
||||
node.kids.forEach((k, i) => {
|
||||
if (k.kids.length === 0) leafRefs.push({ idx: id, pos: i });
|
||||
else {
|
||||
const kidId = nodeId.get(k);
|
||||
if (kidId != null) walk(k, kidId);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (root.kids.length === 0 && root.segs.length >= 3) leafRefs.push({ idx: 0, pos: 0 });
|
||||
else walk(root, 0);
|
||||
extraRoots.forEach((r, i) => {
|
||||
const eid = base.length + spIdx + i;
|
||||
idMap.set(eid, r);
|
||||
if (r.kids.length === 0) leafRefs.push({ idx: eid, pos: 0 });
|
||||
else walk(r, eid);
|
||||
});
|
||||
const resolveRef = (idx, pos) => {
|
||||
const node = idx === 0 ? root : idMap.get(idx);
|
||||
if (!node) return null;
|
||||
if (node.kids.length === 0) return node.segs;
|
||||
if (Number.isInteger(pos) && pos >= 0 && pos < node.kids.length) {
|
||||
const k = node.kids[pos];
|
||||
if (k.kids.length === 0) return k.segs;
|
||||
const l = [];
|
||||
dfsLeaves(k, l);
|
||||
return l[0]?.segs ?? null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return { regions, clips, leafRefs, resolveRef };
|
||||
}
|
||||
function splitPolySegments(region, axis, pos) {
|
||||
const pts = regionPoints(region);
|
||||
const n = pts.length;
|
||||
const get = (p) => axis === "v" ? p.x : p.y;
|
||||
const crossings = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = pts[i], b = pts[(i + 1) % n];
|
||||
const da = get(a) - pos, db = get(b) - pos;
|
||||
if (da < 0 && db > 0 || da > 0 && db < 0) {
|
||||
const t = da / (da - db);
|
||||
crossings.push({
|
||||
i,
|
||||
p: axis === "v" ? { x: pos, y: a.y + (b.y - a.y) * t } : { x: a.x + (b.x - a.x) * t, y: pos }
|
||||
});
|
||||
}
|
||||
}
|
||||
if (crossings.length !== 2) return null;
|
||||
const [c1, c2] = crossings;
|
||||
if (c1.i === c2.i) return null;
|
||||
const collect = (from, to) => {
|
||||
const ps = [from.p];
|
||||
for (let k = (from.i + 1) % n; ; k = (k + 1) % n) {
|
||||
ps.push(pts[k]);
|
||||
if (k === to.i) break;
|
||||
}
|
||||
ps.push(to.p);
|
||||
return ps.filter((p, i) => i === 0 || Math.abs(p.x - ps[i - 1].x) > 0.01 || Math.abs(p.y - ps[i - 1].y) > 0.01);
|
||||
};
|
||||
const toSegs = (ps) => {
|
||||
const segs = [];
|
||||
for (let i = 0; i < ps.length; i++) segs.push({ ps: ps[i], pe: ps[(i + 1) % ps.length], name: "segment" });
|
||||
return segs;
|
||||
};
|
||||
const polyA = collect(c1, c2);
|
||||
const polyB = collect(c2, c1);
|
||||
if (polyA.length < 3 || polyB.length < 3) return null;
|
||||
return { a: toSegs(polyA), b: toSegs(polyB) };
|
||||
}
|
||||
function hmOf(sub) {
|
||||
const hm = sub.hm;
|
||||
return hm ?? {};
|
||||
}
|
||||
function inferSashGlyphs(shape, replayed) {
|
||||
let rr = null;
|
||||
let regions;
|
||||
if (replayed && !Array.isArray(replayed)) {
|
||||
rr = replayed;
|
||||
regions = rr.regions;
|
||||
} else if (replayed) {
|
||||
regions = replayed;
|
||||
} else {
|
||||
rr = replayShape(shape);
|
||||
regions = rr.regions;
|
||||
}
|
||||
const regionBBs = regions.map((r) => bboxOf(regionPoints(r)));
|
||||
const glyphs = [];
|
||||
const bdRect = (bd) => {
|
||||
if (!bd.from || !bd.to) return null;
|
||||
const r = {
|
||||
x0: Math.min(bd.from.x, bd.to.x),
|
||||
y0: Math.min(bd.from.y, bd.to.y),
|
||||
x1: Math.max(bd.from.x, bd.to.x),
|
||||
y1: Math.max(bd.from.y, bd.to.y)
|
||||
};
|
||||
const ok = [r.x0, r.y0, r.x1, r.y1].every((v) => Number.isFinite(v));
|
||||
return ok && !bd.hidden && r.x1 - r.x0 > 10 && r.y1 - r.y0 > 10 ? r : null;
|
||||
};
|
||||
const kindOf = (sub) => String(sub.type) === "Slide" ? "sliding" : "casement";
|
||||
const rectOfRef = (ref) => {
|
||||
if (!ref || !Number.isInteger(ref.idx) || ref.idx < 0) return null;
|
||||
if (rr) {
|
||||
const segs = rr.resolveRef(ref.idx, ref.pos);
|
||||
if (segs && segs.length >= 3) return bboxOf(regionPoints(segs));
|
||||
}
|
||||
const i = ref.idx;
|
||||
return i < regionBBs.length ? regionBBs[i] : null;
|
||||
};
|
||||
const iouR = (a, b) => {
|
||||
const w = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
|
||||
const h = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
|
||||
if (w <= 0 || h <= 0) return 0;
|
||||
const inter = w * h;
|
||||
return inter / ((a.x1 - a.x0) * (a.y1 - a.y0) + (b.x1 - b.x0) * (b.y1 - b.y0) - inter);
|
||||
};
|
||||
const pushGlyphs = (r, entries, sub, forceRect = false) => {
|
||||
const kind = kindOf(sub);
|
||||
const hm = hmOf(sub);
|
||||
for (const e of entries) {
|
||||
const rect = !forceRect ? rectOfRef(e.polyId) ?? r : r;
|
||||
if (glyphs.some((g) => iouR(rect, g) > 0.15)) continue;
|
||||
glyphs.push({
|
||||
x0: rect.x0,
|
||||
y0: rect.y0,
|
||||
x1: rect.x1,
|
||||
y1: rect.y1,
|
||||
od: e.od ?? sub.od ?? 90,
|
||||
openToward: e.openToward ?? sub.openToward ?? hm.openToward,
|
||||
openDirection: e.openDirection ?? hm.openDirection,
|
||||
isDoor: Boolean(e.isDoor || sub.isDoor),
|
||||
wtf: Boolean(e.wtf ?? sub.wtf ?? sub.withTurningFrame),
|
||||
kind
|
||||
});
|
||||
}
|
||||
};
|
||||
const unmatched = [];
|
||||
for (const sub of shape.sm ?? []) {
|
||||
const type = String(sub.type ?? "");
|
||||
if (type === "Screen" || type === "DoubleScreen") continue;
|
||||
const raw = sub.sashes?.length ? sub.sashes : [sub];
|
||||
const entries = raw.filter((e) => e && e.enabled !== false);
|
||||
if (!entries.length) continue;
|
||||
const n = Math.max(entries.length, type.startsWith("Double") ? 2 : 1);
|
||||
let rect = sub.bd ? bdRect(sub.bd) : null;
|
||||
if (!rect) rect = rectOfRef(sub.polyId);
|
||||
if (rect) {
|
||||
for (let k = 0; k < n; k++) {
|
||||
const part = n > 1 ? { x0: rect.x0 + (rect.x1 - rect.x0) * k / n, y0: rect.y0, x1: rect.x0 + (rect.x1 - rect.x0) * (k + 1) / n, y1: rect.y1 } : rect;
|
||||
pushGlyphs(part, [entries[Math.min(k, entries.length - 1)]], sub, n > 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
unmatched.push({ sub, entries });
|
||||
}
|
||||
if (unmatched.length && regions.length) {
|
||||
const flatPts = [];
|
||||
for (const r of regions) {
|
||||
for (const seg of r) {
|
||||
if (Number.isFinite(seg.ps?.x) && Number.isFinite(seg.ps?.y)) flatPts.push(seg.ps);
|
||||
if (Number.isFinite(seg.pe?.x) && Number.isFinite(seg.pe?.y)) flatPts.push(seg.pe);
|
||||
}
|
||||
}
|
||||
const fb = bboxOf(flatPts);
|
||||
const K = unmatched.reduce((s, u) => s + u.entries.length, 0);
|
||||
const colW = (fb.x1 - fb.x0) / K;
|
||||
let i = 0;
|
||||
for (const u of unmatched) {
|
||||
for (const e of u.entries) {
|
||||
glyphs.push({
|
||||
x0: fb.x0 + i * colW,
|
||||
x1: fb.x0 + (i + 1) * colW,
|
||||
y0: fb.y0,
|
||||
y1: fb.y1,
|
||||
od: e.od ?? u.sub.od ?? 90,
|
||||
openToward: e.openToward ?? u.sub.openToward ?? hmOf(u.sub).openToward,
|
||||
openDirection: e.openDirection ?? hmOf(u.sub).openDirection,
|
||||
isDoor: Boolean(e.isDoor || u.sub.isDoor),
|
||||
kind: kindOf(u.sub)
|
||||
});
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { regions, sashes: glyphs };
|
||||
}
|
||||
export {
|
||||
DEC_PRESETS,
|
||||
bboxOf,
|
||||
inferSashGlyphs,
|
||||
parseDrawing,
|
||||
pointInRegion,
|
||||
regionPoints,
|
||||
replayShape,
|
||||
spAxis,
|
||||
spPos,
|
||||
splitPolySegments
|
||||
};
|
||||
@@ -70,6 +70,28 @@ export interface SashShape {
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
/** 装饰格条(自研扩展字段,挂在 FrameShape.decs;ri = 重放区域索引) */
|
||||
export interface DecBar {
|
||||
ri: number
|
||||
kind: string // 风格预设 id(modern/italian/cream/chinese/european/french/medit/pastoral)
|
||||
rows: number // 横向分割条数
|
||||
cols: number // 纵向分割条数
|
||||
bw: number // 条宽 mm
|
||||
color: string
|
||||
}
|
||||
|
||||
/** 格条风格预设(对齐源站 decorationBar 8 种装修风格) */
|
||||
export const DEC_PRESETS: { kind: string; label: string; rows: number; cols: number; bw: number; color: string }[] = [
|
||||
{ kind: 'modern', label: '现代轻奢', rows: 1, cols: 1, bw: 25, color: '#2b2b2b' },
|
||||
{ kind: 'italian', label: '意式简约', rows: 1, cols: 1, bw: 18, color: '#9aa0a6' },
|
||||
{ kind: 'cream', label: '奶油风', rows: 2, cols: 2, bw: 22, color: '#f3ece2' },
|
||||
{ kind: 'chinese', label: '中式', rows: 2, cols: 2, bw: 28, color: '#6b4a2b' },
|
||||
{ kind: 'european', label: '欧式古典', rows: 3, cols: 3, bw: 20, color: '#c9a86a' },
|
||||
{ kind: 'french', label: '法式', rows: 2, cols: 2, bw: 16, color: '#d8c9a3' },
|
||||
{ kind: 'medit', label: '地中海', rows: 2, cols: 3, bw: 24, color: '#3a6ea5' },
|
||||
{ kind: 'pastoral', label: '田园密格', rows: 4, cols: 4, bw: 14, color: '#ffffff' },
|
||||
]
|
||||
|
||||
export interface FrameShape {
|
||||
type: 'Frame' | string
|
||||
polygon?: {
|
||||
@@ -87,6 +109,7 @@ export interface FrameShape {
|
||||
ps?: Record<string, number>
|
||||
uid?: string
|
||||
titl?: string
|
||||
decs?: DecBar[] // 装饰格条(自研扩展)
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user