518 lines
19 KiB
JavaScript
518 lines
19 KiB
JavaScript
/**
|
||
* 把归一化后的图纸文档(Doc)展平成渲染用的「显示列表」。
|
||
*
|
||
* 展平只做一次(加载时),之后缩放平移都不再重算几何。
|
||
* INSERT 会递归展开并把变换烘焙进坐标,因此显示列表里的坐标全是世界坐标。
|
||
*/
|
||
|
||
import ACI from '../libs/dxf-parser/AutoCadColorIndex.js'
|
||
import {
|
||
applyX, applyY, arcBBox, arcPoints, bboxOfPoints, bulgeArcPoints, circlePoints, conformal,
|
||
ellipsePoints, fitPointCurve, growBox, IDENTITY, makeInsertMatrix, matrixScale, mul,
|
||
splinePoints, TAU, transformPoints,
|
||
} from './curves.js'
|
||
import { builtinDefinitionLines, hatchLoops, hatchPatternSegments } from './hatch.js'
|
||
import { parseMText, parseSimpleText } from './mtext.js'
|
||
|
||
const MAX_INSERT_DEPTH = 12
|
||
const MAX_ARRAY_CELLS = 2000
|
||
|
||
/** DWG 线宽枚举(值 = 1/100 mm);其余为特殊值 */
|
||
const LW_TABLE = new Set([0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211])
|
||
const LW_BYLAYER = -1, LW_BYBLOCK = -2, LW_DEFAULT = -3
|
||
|
||
function normLineweight(v) {
|
||
if (v == null) return LW_BYLAYER
|
||
if (v < 0) return v >= -3 ? v : LW_BYLAYER
|
||
if (v === 28) return LW_BYLAYER
|
||
if (v === 29) return LW_BYBLOCK
|
||
if (v === 30 || v === 31) return LW_DEFAULT
|
||
return LW_TABLE.has(v) ? v : LW_DEFAULT
|
||
}
|
||
|
||
/** ACI 索引 → 0xRRGGBB */
|
||
export function aciColor(i) {
|
||
if (i == null || i < 0 || i > 255) return 0x000000
|
||
const c = ACI[i]
|
||
return typeof c === 'number' ? c : 0x000000
|
||
}
|
||
|
||
/**
|
||
* 解析实体最终颜色。
|
||
* @returns {number} 0xRRGGBB
|
||
*/
|
||
function resolveColor(ent, layer, inherited) {
|
||
if (typeof ent.trueColor === 'number') return ent.trueColor & 0xffffff
|
||
const ci = ent.colorIndex
|
||
if (ci === 0) return inherited != null ? inherited : (layer ? layerColor(layer) : 0x000000) // BYBLOCK
|
||
if (ci == null || ci === 256) return layer ? layerColor(layer) : 0x000000 // BYLAYER
|
||
if (ci > 0 && ci < 256) return aciColor(ci)
|
||
return 0x000000
|
||
}
|
||
|
||
function layerColor(layer) {
|
||
if (typeof layer.trueColor === 'number') return layer.trueColor & 0xffffff
|
||
if (layer.colorIndex != null && layer.colorIndex !== 0) return aciColor(Math.abs(layer.colorIndex))
|
||
if (typeof layer.color === 'number' && layer.color !== 16777215) return layer.color & 0xffffff
|
||
return 0x000000
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 主入口
|
||
|
||
/**
|
||
* @param {Doc} doc 归一化文档
|
||
* @param {object} opt { space: 'model' | layout 名 }
|
||
* @returns {{shapes: Array, bbox: number[], stats: object}}
|
||
*/
|
||
export function flatten(doc, opt = {}) {
|
||
const shapes = []
|
||
const bbox = [Infinity, Infinity, -Infinity, -Infinity]
|
||
const stats = { entities: 0, shapes: 0, skipped: 0, byType: {} }
|
||
const ctx = { doc, shapes, bbox, stats, blockStack: new Set() }
|
||
|
||
const entities = opt.entities || doc.modelEntities || []
|
||
for (const ent of entities) {
|
||
emitEntity(ctx, ent, IDENTITY, null, null, 0)
|
||
}
|
||
stats.shapes = shapes.length
|
||
return { shapes, bbox, stats }
|
||
}
|
||
|
||
function emitEntity(ctx, ent, m, inheritColor, inheritLayer, depth) {
|
||
if (!ent || ent.isVisible === false) return
|
||
const type = ent.type
|
||
ctx.stats.entities++
|
||
ctx.stats.byType[type] = (ctx.stats.byType[type] || 0) + 1
|
||
|
||
// 图层:块内实体用 "0" 图层时继承外层图层(AutoCAD 规则)
|
||
let layerName = ent.layer || '0'
|
||
if (layerName === '0' && inheritLayer) layerName = inheritLayer
|
||
const layer = ctx.doc.layers.get(layerName)
|
||
|
||
const base = {
|
||
layer: layerName,
|
||
color: resolveColor(ent, layer, inheritColor),
|
||
lw: normLineweight(ent.lineweight),
|
||
lt: resolveLineType(ent, layer),
|
||
ltScale: ent.lineTypeScale || 1,
|
||
ent,
|
||
}
|
||
|
||
switch (type) {
|
||
case 'LINE': {
|
||
const p = ent.startPoint, q = ent.endPoint
|
||
if (!p || !q) return
|
||
addPoly(ctx, base, new Float64Array([
|
||
applyX(m, p.x, p.y), applyY(m, p.x, p.y),
|
||
applyX(m, q.x, q.y), applyY(m, q.x, q.y),
|
||
]), false)
|
||
break
|
||
}
|
||
case 'CIRCLE':
|
||
addCircleLike(ctx, base, m, ent.center, ent.radius, ent.radius, 0, 0, TAU)
|
||
break
|
||
case 'ARC': {
|
||
const a0 = ang(ent.startAngle), a1 = ang(ent.endAngle)
|
||
addCircleLike(ctx, base, m, ent.center, ent.radius, ent.radius, 0, a0, a1)
|
||
break
|
||
}
|
||
case 'ELLIPSE': {
|
||
const mj = ent.majorAxisEndPoint || { x: 1, y: 0 }
|
||
const rx = Math.hypot(mj.x, mj.y)
|
||
const ry = rx * (ent.axisRatio || 1)
|
||
const rot = Math.atan2(mj.y, mj.x)
|
||
let a0 = ent.startAngle || 0, a1 = ent.endAngle
|
||
if (a1 == null || Math.abs((a1 - a0) % TAU) < 1e-9) { a0 = 0; a1 = TAU }
|
||
addCircleLike(ctx, base, m, ent.center, rx, ry, rot, a0, a1)
|
||
break
|
||
}
|
||
case 'LWPOLYLINE': {
|
||
const pts = lwPolyPoints(ent)
|
||
if (pts.length >= 4) addPoly(ctx, base, transformPoints(pts, m), isClosed(ent.flag))
|
||
break
|
||
}
|
||
case 'POLYLINE':
|
||
case 'POLYLINE2D':
|
||
case 'POLYLINE3D': {
|
||
const pts = polyPoints(ent)
|
||
if (pts.length >= 4) addPoly(ctx, base, transformPoints(pts, m), isClosed(ent.flag))
|
||
break
|
||
}
|
||
case 'SPLINE': {
|
||
const pts = splineEntityPoints(ent)
|
||
if (pts.length >= 4) addPoly(ctx, base, transformPoints(pts, m), !!(ent.flag & 1))
|
||
break
|
||
}
|
||
case 'POINT': {
|
||
const p = ent.position || ent.startPoint || ent.center
|
||
if (!p) return
|
||
push(ctx, { ...base, kind: 'point', x: applyX(m, p.x, p.y), y: applyY(m, p.x, p.y) },
|
||
[applyX(m, p.x, p.y), applyY(m, p.x, p.y), applyX(m, p.x, p.y), applyY(m, p.x, p.y)])
|
||
break
|
||
}
|
||
case 'SOLID':
|
||
case 'TRACE': {
|
||
const cs = [ent.corner1, ent.corner2, ent.corner4, ent.corner3].filter(Boolean)
|
||
if (cs.length < 3) return
|
||
const pts = new Float64Array(cs.length * 2)
|
||
cs.forEach((c, i) => { pts[i * 2] = applyX(m, c.x, c.y); pts[i * 2 + 1] = applyY(m, c.x, c.y) })
|
||
push(ctx, { ...base, kind: 'fill', loops: [pts] }, bboxOfPoints(pts))
|
||
break
|
||
}
|
||
case '3DFACE': {
|
||
const cs = [ent.corner1 || ent.vertices?.[0], ent.corner2 || ent.vertices?.[1],
|
||
ent.corner3 || ent.vertices?.[2], ent.corner4 || ent.vertices?.[3]].filter(Boolean)
|
||
if (cs.length < 3) return
|
||
const pts = new Float64Array(cs.length * 2)
|
||
cs.forEach((c, i) => { pts[i * 2] = applyX(m, c.x, c.y); pts[i * 2 + 1] = applyY(m, c.x, c.y) })
|
||
addPoly(ctx, base, pts, true)
|
||
break
|
||
}
|
||
case 'HATCH':
|
||
emitHatch(ctx, base, ent, m)
|
||
break
|
||
case 'TEXT':
|
||
case 'ATTRIB':
|
||
case 'ATTDEF':
|
||
emitText(ctx, base, ent, m)
|
||
break
|
||
case 'MTEXT':
|
||
emitMText(ctx, base, ent, m)
|
||
break
|
||
case 'DIMENSION':
|
||
case 'ARC_DIMENSION':
|
||
emitBlockRef(ctx, base, ent, ent.name, m, inheritColor, layerName, depth, ent.definitionPoint)
|
||
break
|
||
case 'LEADER':
|
||
emitLeader(ctx, base, ent, m)
|
||
break
|
||
case 'MULTILEADER':
|
||
case 'MLEADER':
|
||
emitMLeader(ctx, base, ent, m)
|
||
break
|
||
case 'INSERT':
|
||
emitInsert(ctx, base, ent, m, inheritColor, layerName, depth)
|
||
break
|
||
case 'VIEWPORT':
|
||
case 'SEQEND':
|
||
case 'VERTEX':
|
||
case 'BLOCK':
|
||
case 'ENDBLK':
|
||
break
|
||
default:
|
||
ctx.stats.skipped++
|
||
break
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 各类实体
|
||
|
||
function resolveLineType(ent, layer) {
|
||
let lt = ent.lineType || ent.lineTypeName
|
||
if (!lt || lt === 'ByLayer' || lt === 'BYLAYER') lt = layer ? layer.lineType : null
|
||
if (!lt || lt === 'ByBlock' || lt === 'BYBLOCK') return null
|
||
if (/^continuous$/i.test(lt)) return null
|
||
return lt
|
||
}
|
||
|
||
const isClosed = (flag) => !!(flag & 1)
|
||
/**
|
||
* 文档内的角度约定:一律弧度。
|
||
* libredwg 本身就给弧度;dxf-parser 混用(圆弧是弧度、rotation 是度),
|
||
* 已在 source.js 的归一化里统一,这里不再换算。
|
||
*/
|
||
const ang = (a) => a || 0
|
||
|
||
function lwPolyPoints(ent) {
|
||
const vs = ent.vertices || []
|
||
if (!vs.length) return new Float64Array(0)
|
||
const out = []
|
||
const closed = isClosed(ent.flag)
|
||
const n = vs.length
|
||
out.push(vs[0].x, vs[0].y)
|
||
for (let i = 0; i < (closed ? n : n - 1); i++) {
|
||
const a = vs[i], b = vs[(i + 1) % n]
|
||
if (a.bulge) out.push(...bulgeArcPoints(a.x, a.y, b.x, b.y, a.bulge))
|
||
else out.push(b.x, b.y)
|
||
}
|
||
return new Float64Array(out)
|
||
}
|
||
|
||
function polyPoints(ent) {
|
||
const vs = (ent.vertices || []).filter((v) => v && v.x != null)
|
||
if (!vs.length) return new Float64Array(0)
|
||
// 曲线拟合的多段线(flag & 4 = spline-fit):顶点已是拟合点,直接连
|
||
const out = []
|
||
const closed = isClosed(ent.flag)
|
||
const n = vs.length
|
||
out.push(vs[0].x, vs[0].y)
|
||
for (let i = 0; i < (closed ? n : n - 1); i++) {
|
||
const a = vs[i], b = vs[(i + 1) % n]
|
||
if (a.bulge) out.push(...bulgeArcPoints(a.x, a.y, b.x, b.y, a.bulge))
|
||
else out.push(b.x, b.y)
|
||
}
|
||
return new Float64Array(out)
|
||
}
|
||
|
||
function splineEntityPoints(ent) {
|
||
const cps = ent.controlPoints || []
|
||
if (cps.length >= 2) {
|
||
const weights = ent.weights
|
||
const pts = cps.map((c, i) => ({ x: c.x, y: c.y, weight: weights ? weights[i] : c.weight }))
|
||
return splinePoints(ent.degree || 3, pts, ent.knots, !!(ent.flag & 1))
|
||
}
|
||
const fps = ent.fitPoints || []
|
||
if (fps.length >= 2) return fitPointCurve(fps, !!(ent.flag & 1))
|
||
return new Float64Array(0)
|
||
}
|
||
|
||
function addPoly(ctx, base, pts, closed) {
|
||
if (pts.length < 4) return
|
||
push(ctx, { ...base, kind: 'poly', pts, closed }, bboxOfPoints(pts))
|
||
}
|
||
|
||
/**
|
||
* 圆/圆弧/椭圆:变换若是共形的(等比缩放+旋转),保留解析形式交给
|
||
* canvas 的 ellipse() 精确绘制;否则退化成折线。
|
||
*/
|
||
function addCircleLike(ctx, base, m, center, rx, ry, rot, a0, a1) {
|
||
if (!center) return
|
||
if (!(rx > 0)) return
|
||
const full = Math.abs(a1 - a0) >= TAU - 1e-9
|
||
const cf = conformal(m)
|
||
if (cf) {
|
||
// 共形变换下圆仍是圆、椭圆仍是椭圆,保留解析形式让 canvas 画出无锯齿的曲线。
|
||
// 镜像会让参数角反向:a → -a,且弧的走向翻转,故起止角对调。
|
||
const s = cf.scale, phi = cf.rotation
|
||
const R = cf.mirror ? phi - rot : rot + phi
|
||
const A0 = cf.mirror ? -a1 : a0
|
||
const A1 = cf.mirror ? -a0 : a1
|
||
const cx = applyX(m, center.x, center.y)
|
||
const cy = applyY(m, center.x, center.y)
|
||
const RX = rx * s, RY = ry * s
|
||
push(ctx, { ...base, kind: 'arc', cx, cy, rx: RX, ry: RY, rot: R, a0: A0, a1: A1 },
|
||
arcBBox(cx, cy, RX, RY, R, A0, A1))
|
||
return
|
||
}
|
||
const pts = rx === ry
|
||
? (full ? circlePoints(center.x, center.y, rx) : arcPoints(center.x, center.y, rx, a0, a1))
|
||
: ellipsePoints(center.x, center.y, rx * Math.cos(rot), rx * Math.sin(rot), ry / rx, a0, a1)
|
||
addPoly(ctx, base, transformPoints(pts, m), full)
|
||
}
|
||
|
||
function emitHatch(ctx, base, ent, m) {
|
||
const loops = hatchLoops(ent)
|
||
if (!loops.length) return
|
||
const solid = ent.solidFill === 1 || /^solid$/i.test(ent.patternName || '')
|
||
if (solid) {
|
||
const world = loops.map((l) => transformPoints(l, m))
|
||
const bb = [Infinity, Infinity, -Infinity, -Infinity]
|
||
world.forEach((l) => growBox(bb, bboxOfPoints(l)))
|
||
push(ctx, { ...base, kind: 'fill', loops: world }, bb)
|
||
return
|
||
}
|
||
// 文件里没带定义线时(预定义图案只存名字),从内置图案表展开
|
||
let defs = ent.definitionLines
|
||
if (!defs || !defs.length) {
|
||
defs = builtinDefinitionLines(ent.patternName, ent.patternAngle || 0, ent.patternScale || 1)
|
||
}
|
||
const segs = hatchPatternSegments(loops, defs)
|
||
if (!segs.length) {
|
||
// 没有图案定义线(例如未知图案):至少把边界画出来
|
||
for (const l of loops) addPoly(ctx, base, transformPoints(l, m), true)
|
||
return
|
||
}
|
||
const arr = transformPoints(new Float64Array(segs), m)
|
||
push(ctx, { ...base, kind: 'segs', pts: arr }, bboxOfPoints(arr))
|
||
}
|
||
|
||
function emitText(ctx, base, ent, m) {
|
||
const raw = ent.text
|
||
if (raw == null || raw === '') return
|
||
const txt = parseSimpleText(raw)
|
||
if (!txt) return
|
||
const style = ctx.doc.styles.get(ent.styleName) || null
|
||
const sc = matrixScale(m)
|
||
const h = (ent.textHeight || (style && style.fixedTextHeight) || 2.5) * sc
|
||
if (!(h > 0)) return
|
||
// TEXT 的对齐点:halign/valign 非默认时用 endPoint(第二对齐点)
|
||
const useAlt = (ent.halign && ent.halign !== 0) || (ent.valign && ent.valign !== 0)
|
||
const p = (useAlt && ent.endPoint && (ent.endPoint.x || ent.endPoint.y)) ? ent.endPoint : (ent.startPoint || ent.position)
|
||
if (!p) return
|
||
const cf = conformal(m)
|
||
const rot = ang(ent.rotation) + (cf ? cf.rotation : 0)
|
||
push(ctx, {
|
||
...base,
|
||
kind: 'text',
|
||
x: applyX(m, p.x, p.y),
|
||
y: applyY(m, p.x, p.y),
|
||
h,
|
||
rot,
|
||
wFactor: ent.xScale || (style && style.widthFactor) || 1,
|
||
oblique: ang(ent.obliqueAngle || (style && style.obliqueAngle) || 0),
|
||
halign: ent.halign || 0,
|
||
valign: ent.valign || 0,
|
||
mirrorX: !!(ent.generationFlag & 2),
|
||
mirrorY: !!(ent.generationFlag & 4),
|
||
font: style && style.font,
|
||
runs: [{ text: txt, hFactor: 1, wFactor: 1, rise: 0 }],
|
||
plain: txt,
|
||
}, textBox(applyX(m, p.x, p.y), applyY(m, p.x, p.y),
|
||
charWidth(txt) * h * (ent.xScale || 1), h,
|
||
ent.halign === 1 || ent.halign === 4 ? 0.5 : ent.halign === 2 ? 1 : 0,
|
||
ent.valign === 3 ? 1 : ent.valign === 2 || ent.halign === 4 ? 0.5 : 0,
|
||
rot))
|
||
}
|
||
|
||
/** 估算字符串宽度(以字高为单位):中日韩按 1 个字宽,西文按 0.55 */
|
||
function charWidth(t) {
|
||
let w = 0
|
||
for (const c of t) w += c.charCodeAt(0) > 0x2e80 ? 1 : 0.55
|
||
return w
|
||
}
|
||
|
||
/**
|
||
* 文字包围盒。
|
||
* @param ax 0=左 0.5=中 1=右;ay 0=基线/底 0.5=中 1=顶
|
||
*/
|
||
function textBox(x, y, w, h, ax, ay, rot) {
|
||
const x0 = -w * ax, x1 = x0 + w
|
||
const y1 = h * (1 - ay) + h * 0.25 // 多留一点给下伸部
|
||
const y0 = y1 - h * 1.25
|
||
const c = Math.cos(rot || 0), s = Math.sin(rot || 0)
|
||
const b = [Infinity, Infinity, -Infinity, -Infinity]
|
||
for (const [lx, ly] of [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]) {
|
||
const px = x + lx * c - ly * s
|
||
const py = y + lx * s + ly * c
|
||
if (px < b[0]) b[0] = px
|
||
if (py < b[1]) b[1] = py
|
||
if (px > b[2]) b[2] = px
|
||
if (py > b[3]) b[3] = py
|
||
}
|
||
return b
|
||
}
|
||
|
||
/** MTEXT 的 9 种对齐点 */
|
||
const MT_H = { 1: 0, 2: 0.5, 3: 1, 4: 0, 5: 0.5, 6: 1, 7: 0, 8: 0.5, 9: 1 }
|
||
const MT_V = { 1: 1, 2: 1, 3: 1, 4: 0.5, 5: 0.5, 6: 0.5, 7: 0, 8: 0, 9: 0 }
|
||
|
||
function emitMText(ctx, base, ent, m) {
|
||
const { lines } = parseMText(ent.text)
|
||
const plain = lines.map((l) => l.map((r) => r.text).join('')).join('\n')
|
||
if (!plain.trim()) return
|
||
const style = ctx.doc.styles.get(ent.styleName) || null
|
||
const sc = matrixScale(m)
|
||
const h = (ent.textHeight || 2.5) * sc
|
||
const p = ent.insertionPoint
|
||
if (!p || !(h > 0)) return
|
||
const cf = conformal(m)
|
||
let rot = 0
|
||
if (ent.direction && (ent.direction.x || ent.direction.y)) rot = Math.atan2(ent.direction.y, ent.direction.x)
|
||
else rot = ang(ent.rotation)
|
||
rot += cf ? cf.rotation : 0
|
||
|
||
const ap = ent.attachmentPoint || 1
|
||
const lineGap = (ent.lineSpacing || 1) * 1.667 // AutoCAD 默认行距系数
|
||
const ax = MT_H[ap] ?? 0
|
||
const ay = MT_V[ap] ?? 1
|
||
const width = (ent.rectWidth || 0) * sc
|
||
const X = applyX(m, p.x, p.y), Y = applyY(m, p.x, p.y)
|
||
|
||
// 包围盒:有 rectWidth 就按框宽算(正文会在这个宽度内折行),
|
||
// 否则按最长一行估。行数同样按折行后估,否则长段落的盒子会短一截。
|
||
const longest = Math.max(...lines.map((l) => charWidth(l.map((r) => r.text).join(''))), 1)
|
||
const boxW = width > 0 ? width : longest * h
|
||
const nLines = width > 0
|
||
? lines.reduce((n, l) => n + Math.max(1, Math.ceil((charWidth(l.map((r) => r.text).join('')) * h) / width)), 0)
|
||
: lines.length
|
||
const boxH = Math.max(1, nLines) * h * lineGap
|
||
|
||
push(ctx, {
|
||
...base, kind: 'mtext', x: X, y: Y, h, rot, lines, lineGap, ax, ay, width,
|
||
font: style && style.font, plain,
|
||
}, textBox(X, Y, boxW, boxH, ax, ay, rot))
|
||
}
|
||
|
||
function emitLeader(ctx, base, ent, m) {
|
||
const vs = ent.vertices || ent.points || []
|
||
if (vs.length < 2) return
|
||
const pts = new Float64Array(vs.length * 2)
|
||
vs.forEach((v, i) => { pts[i * 2] = applyX(m, v.x, v.y); pts[i * 2 + 1] = applyY(m, v.x, v.y) })
|
||
addPoly(ctx, base, pts, false)
|
||
}
|
||
|
||
function emitMLeader(ctx, base, ent, m) {
|
||
const ctxData = ent.contextData || ent.context || {}
|
||
const leaders = ctxData.leaders || ent.leaders || []
|
||
for (const ld of leaders) {
|
||
for (const line of ld.leaderLines || ld.lines || []) {
|
||
const vs = line.vertices || line.points || []
|
||
if (vs.length < 2) continue
|
||
const pts = new Float64Array(vs.length * 2)
|
||
vs.forEach((v, i) => { pts[i * 2] = applyX(m, v.x, v.y); pts[i * 2 + 1] = applyY(m, v.x, v.y) })
|
||
addPoly(ctx, base, pts, false)
|
||
}
|
||
}
|
||
const t = ctxData.text || ctxData.textLabel
|
||
if (t && ctxData.textLocation) {
|
||
emitMText(ctx, base, {
|
||
text: t, insertionPoint: ctxData.textLocation, textHeight: ctxData.textHeight || ent.textHeight,
|
||
rotation: 0, attachmentPoint: ctxData.textAttachmentPoint || 1, styleName: ctxData.textStyleName,
|
||
}, m)
|
||
}
|
||
}
|
||
|
||
/** DIMENSION:真正的图形放在一个匿名块里,直接展开那个块 */
|
||
function emitBlockRef(ctx, base, ent, blockName, m, inheritColor, layerName, depth, fallbackPt) {
|
||
const blk = blockName && ctx.doc.blocks.get(blockName)
|
||
if (!blk) return
|
||
if (depth > MAX_INSERT_DEPTH) return
|
||
const bp = blk.basePoint || { x: 0, y: 0 }
|
||
const bm = mul(m, [1, 0, 0, 1, -bp.x, -bp.y])
|
||
for (const e of blk.entities || []) {
|
||
emitEntity(ctx, e, bm, base.color, layerName, depth + 1)
|
||
}
|
||
}
|
||
|
||
function emitInsert(ctx, base, ent, m, inheritColor, layerName, depth) {
|
||
if (depth > MAX_INSERT_DEPTH) return
|
||
const blk = ctx.doc.blocks.get(ent.name)
|
||
// ATTRIB 无论块在不在都要画
|
||
for (const a of ent.attribs || []) {
|
||
if (a && a.isVisible !== false) emitEntity(ctx, a, m, base.color, layerName, depth + 1)
|
||
}
|
||
if (!blk) { ctx.stats.skipped++; return }
|
||
const p = ent.insertionPoint || { x: 0, y: 0 }
|
||
const sx = ent.xScale === 0 || ent.xScale == null ? 1 : ent.xScale
|
||
const sy = ent.yScale === 0 || ent.yScale == null ? 1 : ent.yScale
|
||
const rot = ang(ent.rotation)
|
||
const bp = blk.basePoint || { x: 0, y: 0 }
|
||
|
||
const cols = Math.max(1, ent.columnCount || 1)
|
||
const rows = Math.max(1, ent.rowCount || 1)
|
||
const cSp = ent.columnSpacing || 0
|
||
const rSp = ent.rowSpacing || 0
|
||
if (cols * rows > MAX_ARRAY_CELLS) { ctx.stats.skipped++; return }
|
||
|
||
for (let r = 0; r < rows; r++) {
|
||
for (let c = 0; c < cols; c++) {
|
||
const local = makeInsertMatrix(p.x + c * cSp, p.y + r * rSp, sx, sy, rot)
|
||
// 先把块基点移到原点,再套插入变换
|
||
const bm = mul(mul(m, local), [1, 0, 0, 1, -bp.x, -bp.y])
|
||
for (const e of blk.entities || []) {
|
||
emitEntity(ctx, e, bm, base.color, layerName, depth + 1)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 输出
|
||
|
||
function push(ctx, shape, bb) {
|
||
if (!bb || !isFinite(bb[0]) || !isFinite(bb[2])) return
|
||
shape.bbox = bb
|
||
shape.i = ctx.shapes.length
|
||
ctx.shapes.push(shape)
|
||
growBox(ctx.bbox, bb)
|
||
}
|