feat(All):Initial

This commit is contained in:
2025-10-04 23:36:07 +08:00
commit 2b4e5d2668
1321 changed files with 415958 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
import {
EventDispatcher,
Matrix4,
Plane,
Raycaster,
Vector2,
Vector3
} from 'three';
const _plane = new Plane();
const _raycaster = new Raycaster();
const _pointer = new Vector2();
const _offset = new Vector3();
const _intersection = new Vector3();
const _worldPosition = new Vector3();
const _inverseMatrix = new Matrix4();
class DragControls extends EventDispatcher {
constructor( _objects, _camera, _domElement ) {
super();
_domElement.style.touchAction = 'none'; // disable touch scroll
let _selected = null, _hovered = null;
const _intersections = [];
//
const scope = this;
function activate() {
_domElement.addEventListener( 'pointermove', onPointerMove );
_domElement.addEventListener( 'pointerdown', onPointerDown );
_domElement.addEventListener( 'pointerup', onPointerCancel );
_domElement.addEventListener( 'pointerleave', onPointerCancel );
}
function deactivate() {
_domElement.removeEventListener( 'pointermove', onPointerMove );
_domElement.removeEventListener( 'pointerdown', onPointerDown );
_domElement.removeEventListener( 'pointerup', onPointerCancel );
_domElement.removeEventListener( 'pointerleave', onPointerCancel );
_domElement.style.cursor = '';
}
function dispose() {
deactivate();
}
function getObjects() {
return _objects;
}
function getRaycaster() {
return _raycaster;
}
function onPointerMove( event ) {
if ( scope.enabled === false) return;
updatePointer( event );
_raycaster.setFromCamera( _pointer, _camera );
if (_selected) {
if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
}
scope.dispatchEvent( { type: 'drag', object: _selected } );
return;
}
// hover support
if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
_intersections.length = 0;
_raycaster.setFromCamera( _pointer, _camera );
_raycaster.intersectObjects( _objects, scope.recursive, _intersections );
if ( _intersections.length > 0 ) {
const object = _intersections[ 0 ].object.parent;
_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
if ( _hovered !== object && _hovered !== null ) {
scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
_domElement.style.cursor = 'auto';
_hovered = null;
}
if ( _hovered !== object ) {
scope.dispatchEvent( { type: 'hoveron', object } );
_domElement.style.cursor = 'pointer';
_hovered = object;
}
} else {
if ( _hovered !== null ) {
scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
_domElement.style.cursor = 'auto';
_hovered = null;
}
}
}
}
function onPointerDown( event ) {
if (scope.enabled === false || event.button !== 0) return;
updatePointer( event );
_intersections.length = 0;
_raycaster.setFromCamera( _pointer, _camera );
_raycaster.intersectObjects( _objects, scope.recursive, _intersections );
if ( _intersections.length > 0 ) {
_selected = _intersections[ 0 ].object.parent;
_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
_inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
_offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
}
_domElement.style.cursor = 'move';
scope.dispatchEvent( { type: 'dragstart', object: _selected } );
}
}
function onPointerCancel(event) {
if (scope.enabled === false || event.button !== 0) return;
if (_selected) {
scope.dispatchEvent( { type: 'dragend', object: _selected } );
_selected = null;
}
_domElement.style.cursor = _hovered ? 'pointer' : 'auto';
}
function updatePointer( event ) {
const rect = _domElement.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
}
activate();
// API
this.enabled = true;
this.recursive = true;
this.activate = activate;
this.deactivate = deactivate;
this.dispose = dispose;
this.getObjects = getObjects;
this.getRaycaster = getRaycaster;
}
}
export { DragControls };
+88
View File
@@ -0,0 +1,88 @@
import round10 from './round10'
/**
* 复制和移植到代码标准,因为b样条库不再维护。
* Source:
* https://github.com/thibauts/b-spline
*/
export default (t:number, degree:number, points, knots, weights?:number[] | undefined) => {
const n = points.length // 点数量
const d = points[0].length // 分维数
if ((t < 0) || (t > 1)) {
throw new Error('t 在区间 [0,1] 外: t = ' + t)
}
if (degree < 1) throw new Error('degree 必须至少为1(线性)')
if (degree > (n - 1)) throw new Error('degree 必须小于或等于点数量 - 1')
if (!weights) {
// 构建长度为[n]的权重向量
weights = []
for (let i = 0; i < n; i++) {
weights[i] = 1
}
}
if (!knots) {
// 建立长度为[n + degree + 1]的结向量
knots = []
for (let i = 0; i < n + degree + 1; i++) {
knots[i] = i
}
} else {
if (knots.length !== n + degree + 1) throw new Error('坏结矢量长度')
}
const domain = [
degree,
knots.length - 1 - degree
]
// 将t重新映射到定义样条的域
const low = knots[domain[0]]
const high = knots[domain[1]]
t = t * (high - low) + low
// 夹紧到上限和下限,而不是像在原始库中那样抛出错误
t = Math.max(t, low)
t = Math.min(t, high)
// 为提供的[t]值找到s(样条段)
let s
for (s = domain[0]; s < domain[1]; s++) {
if (t >= knots[s] && t <= knots[s + 1]) {
break
}
}
// 将点转换为齐次坐标
const v:any = []
for (let i = 0; i < n; i++) {
v[i] = []
for (let j = 0; j < d; j++) {
v[i][j] = points[i][j] * weights[i]
}
v[i][d] = weights[i]
}
// l (level) 从 1 到 curve degree + 1
let alpha
for (let l = 1; l <= degree + 1; l++) {
// build level l of the pyramid
for (let i = s; i > s - degree - 1 + l; i--) {
alpha = (t - knots[i]) / (knots[i + degree + 1 - l] - knots[i])
// interpolate each component
for (let j = 0; j < d + 1; j++) {
v[i][j] = (1 - alpha) * v[i - 1][j] + alpha * v[i][j]
}
}
}
// 转换回笛卡尔坐标,然后返回
const result:number[] = []
for (let i = 0; i < d; i++) {
result[i] = round10(v[s][i] / v[s][d], -9)
}
return result
}
+400
View File
@@ -0,0 +1,400 @@
import * as THREE from "three";
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls.js";
import {stopRender, renderLoop, render} from "./drawShare"
import {DragControls} from './DragControls.js';
let cpmFn, cpdFn, cpuFn;
function throttle(func, wait) {
let timer: NodeJS.Timeout | null = null;
return function (e) {
if (timer) return;
timer = setTimeout(function () {
func(e);
timer = null;
}, wait)
}
}
/**
* three中绘制矩形
* @param {THREE.Scene} scene 场景
**/
export class DrawRect {
static RectShapeMaterial = new THREE.MeshBasicMaterial({
transparent: true,
opacity: 0.1,
color: 0xccebff,
side: THREE.FrontSide
});
static LineMaterial = new THREE.LineBasicMaterial({
color: 0x15FF00,
linewidth: 10,
linecap: 'round',
linejoin: 'round'
})
static HoverLineMaterial = new THREE.LineBasicMaterial({
color: 0xff0015,
linewidth: 10,
linecap: 'round',
linejoin: 'round'
})
private canvas: HTMLCanvasElement;
private scene: THREE.Scene;
private camera: THREE.OrthographicCamera;
private controls: OrbitControls | undefined;
private signal: (args: { type: 'add' | 'remove' | 'dispatch', name: string, data?: any }) => void;
private middleObject: any;
private readonly group: THREE.Group;
private readonly dragObjects: THREE.Group[] = [];
private readonly dragControls: DragControls;
private isDrag: boolean = false;
private tempModelUuid: string | undefined;
private tempRect: THREE.Group | undefined;
private downPoint: THREE.Vector2 = new THREE.Vector2();
constructor(canvas: HTMLCanvasElement, scene: THREE.Scene, camera: THREE.OrthographicCamera, signal, middleObject) {
this.canvas = canvas;
this.scene = scene;
this.camera = camera;
this.signal = signal;
this.middleObject = middleObject;
middleObject.markList || (middleObject.markList = []); // 矩形标记数组
middleObject.selectRectIndex || (middleObject.selectRectIndex = -1); // 当前选中的矩形下标
middleObject.selectRect = {}; // 当前选中的矩形数据
middleObject.hoverRectIndex = -1; // 当前鼠标经过的矩形的下标
this.group = new THREE.Group();
this.group.name = "cadDrawRect";
this.scene.add(this.group);
this.dragControls = new DragControls(this.dragObjects, camera, canvas);
this.dragControls.addEventListener("hoveron", (event) => {
if(this.isDrag) return;
this.isDrag = true;
this.controls && (this.controls.enabled = false);
this.group.children.forEach((child, index) => {
if (child.userData.rect.modelUuid !== event.object.userData.rect.modelUuid) return;
this.middleObject.hoverRectIndex = index;
child.children.forEach((c:any) => {
if(c.isLine){
c.material = DrawRect.HoverLineMaterial;
}
})
});
});
this.dragControls.addEventListener("dragstart", () => {
// 有上一次选中
if(this.middleObject.selectRectIndex !== -1){
this.group.children[this.middleObject.selectRectIndex]?.children.forEach((c:any) => {
if(c.isLine){
c.material = DrawRect.LineMaterial;
}
})
// 两次选中同一个则取消
if(this.middleObject.selectRectIndex === this.middleObject.hoverRectIndex){
this.middleObject.selectRectIndex = -1;
this.middleObject.selectRect = {};
this.signal({
type: "dispatch",
name: "objectFocusByUuid",
data: [undefined]
})
return;
}
}
this.middleObject.selectRectIndex = this.middleObject.hoverRectIndex;
this.middleObject.selectRect = this.group.children[this.middleObject.selectRectIndex]?.userData.rect;
this.signal({
type: "dispatch",
name: "objectFocusByUuid",
data: [this.middleObject.selectRect.modelUuid]
})
});
this.dragControls.addEventListener("drag", throttle(render,50));
this.dragControls.addEventListener("dragend", (event) => {
if (!event.object) return;
const rectItem = event.object.userData.rect;
rectItem.x += event.object.position.x;
rectItem.y += event.object.position.y;
this.signal({
type: "dispatch",
name: "drawingMarkDone",
data: ["update", rectItem]
})
});
this.dragControls.addEventListener("hoveroff", () => {
if(!this.isDrag) return;
this.isDrag = false;
this.controls && (this.controls.enabled = true);
// 经过的模型未被选择
if(this.middleObject.selectRectIndex !== this.middleObject.hoverRectIndex){
this.group.children[this.middleObject.hoverRectIndex].children.forEach((c:any) => {
if(c.isLine){
c.material = DrawRect.LineMaterial;
}
})
}
this.middleObject.hoverRectIndex = -1;
render();
});
this.dragControls.enabled = true;
cpmFn = throttle(this.onpointermove.bind(this), 16);
cpdFn = this.onpointerdown.bind(this);
cpuFn = this.onpointerup.bind(this);
this.init();
}
init() {
// 若list长度不为0, 则显示已标记框
if (this.middleObject.markList.length !== 0) {
this.middleObject.markList.forEach((item: IDrawingMark) => {
const rectObject = this.draw(item);
this.dragObjects.push(rectObject);
});
}
}
setList({list}) {
this.middleObject.markList = list;
this.init();
}
// 设置controls
setControls(controls: OrbitControls) {
this.controls = controls;
}
// 设置选中
setSelect({modelUuid}){
// 先去除上一次选中
this.group.children[this.middleObject.selectRectIndex]?.children.forEach((c:any) => {
if(c.isLine){
c.material = DrawRect.LineMaterial;
}
})
this.middleObject.selectRectIndex = -1;
this.middleObject.selectRect = {};
if(modelUuid === undefined) return;
this.group.children.forEach((child, index) => {
if (child.userData.rect?.modelUuid !== modelUuid) return;
this.middleObject.selectRectIndex = index;
this.middleObject.selectRect = child.userData.rect;
child.children.forEach((c:any) => {
if(c.isLine){
c.material = DrawRect.HoverLineMaterial;
}
})
});
}
// 还原rect
restoreRect({rect}) {
this.group.children.forEach((child, index) => {
if (child.userData.rect.modelUuid !== rect.modelUuid) return;
const re = JSON.parse(JSON.stringify(child.userData.rect));
this.middleObject.markList.splice(index, 1, re);
this.group.remove(child);
const rectObject = this.draw(re);
this.dragObjects.splice(this.dragObjects.findIndex(item => item.userData.rect.modelUuid === rect.modelUuid), 1, rectObject);
})
}
/**
* 准备开始画矩形标记框
*/
addRect({modelUuid}) {
if (!this.controls) return;
this.tempModelUuid = modelUuid;
this.controls.enabled = false;
this.dragControls.enabled = false;
this.canvas.style.cursor = "crosshair";
stopRender();
this.canvas.addEventListener("pointerdown", cpdFn);
}
// 删除当前选中的模型
deleteRect({modelUuid}){
this.group.children.forEach((child, index) => {
if (child.userData.rect.modelUuid !== modelUuid) return;
this.middleObject.markList.splice(index, 1);
this.group.remove(child);
this.dragObjects.splice(this.dragObjects.findIndex(item => item.userData.rect.modelUuid === modelUuid), 1);
})
}
// 鼠标按下事件
onpointerdown(event: PointerEvent) {
event.stopPropagation();
if (this.isDrag || event.button !== 0) return;
const wp = this.screenToScenePosition(event);
this.downPoint = new THREE.Vector2(wp.x, wp.y);
this.canvas.addEventListener("pointermove", cpmFn);
this.canvas.addEventListener("pointerup", cpuFn);
}
// 鼠标移动事件
onpointermove(event: PointerEvent) {
event.stopPropagation();
if (this.isDrag) return;
const wp = this.screenToScenePosition(event);
this.tempRect && this.group.remove(this.tempRect);
const item: IDrawingMark = {
x: this.downPoint.x,
y: this.downPoint.y,
w: wp.x - this.downPoint.x,
h: wp.y - this.downPoint.y,
modelUuid: this.tempModelUuid
};
this.tempRect = this.draw(item);
}
// 鼠标抬起事件
onpointerup(event: PointerEvent) {
event.stopPropagation();
if (this.isDrag) return;
const wp = this.screenToScenePosition(event);
const item: IDrawingMark = {
x: this.downPoint.x,
y: this.downPoint.y,
w: wp.x - this.downPoint.x,
h: wp.y - this.downPoint.y,
modelUuid: this.tempModelUuid
};
this.drawDone(item)
this.signal({
type: "dispatch",
name: "drawingMarkDone",
data: ["add", item]
})
this.canvas.style.cursor = "default";
this.tempModelUuid = undefined;
this.canvas.removeEventListener("pointermove", cpmFn);
this.canvas.removeEventListener("pointerup", cpuFn);
}
// 取消设置名称去除对应绘制的方法
clearTemp() {
this.tempRect && this.group.remove(this.tempRect);
this.exit();
}
// 名称设置完成后调用的绘制结束方法
drawDone(rect:IDrawingMark) {
this.tempRect && this.group.remove(this.tempRect);
this.middleObject.markList.push(rect);
const rectObject = this.draw(rect);
this.dragObjects.push(rectObject);
this.exit();
}
exit() {
this.controls && (this.controls.enabled = true);
this.dragControls.enabled = true;
renderLoop();
this.canvas.removeEventListener("pointerdown", cpdFn);
}
// 绘制
draw(item: IDrawingMark) {
/* 第二种方式 */
const g = new THREE.Group();
g.name = "mark-rect"
const rectShape = new THREE.Shape();
rectShape.moveTo(item.x, item.y);
rectShape.lineTo(item.x + item.w, item.y);
rectShape.lineTo(item.x + item.w, item.y + item.h);
rectShape.lineTo(item.x, item.y + item.h);
rectShape.lineTo(item.x, item.y);
const geometry = new THREE.ShapeGeometry(rectShape);
const material = DrawRect.RectShapeMaterial;
const mesh = new THREE.Mesh(geometry, material);
g.add(mesh);
//绘制边框线
const lineGeom = new THREE.EdgesGeometry(geometry)
const lineMaterial = DrawRect.LineMaterial;
const line = new THREE.LineSegments(lineGeom, lineMaterial)
line.scale.copy(mesh.scale)
line.rotation.copy(mesh.rotation)
line.position.copy(mesh.position)
g.add(line)
g.userData.rect = item;
this.group.add(g);
// helpRender();
render();
return g;
}
screenToScenePosition(event) {
const offsetX = event.clientX - this.canvas.offsetLeft;
const offsetY = event.clientY - this.canvas.offsetTop;
const screenPosition = new THREE.Vector3(offsetX, offsetY, 0);
//const worldPosition = new THREE.Vector3();
screenPosition.x = (screenPosition.x / this.canvas.clientWidth) * 2 - 1;
screenPosition.y = -(screenPosition.y / this.canvas.clientHeight) * 2 + 1;
screenPosition.z = 0.5;
let p = screenPosition.unproject(this.camera);
return new THREE.Vector2(p.x, p.y);
// screenPosition.sub(this.camera.position).normalize();
// const distance = -this.camera.position.z / screenPosition.z;
// worldPosition.copy(this.camera.position).add(screenPosition.multiplyScalar(distance));
//
// return worldPosition;
}
}
+922
View File
@@ -0,0 +1,922 @@
import * as THREE from "three";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { TTFLoader } from "three/examples/jsm/loaders/TTFLoader.js";
// import { SSAARenderPass } from "three/examples/jsm/postprocessing/SSAARenderPass.js";
// import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
// import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
// import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
// import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
import {
addTriangleFacingCamera,
getBSplinePolyline, getBulgeCurvePoints,
mtextContentAndFormattingToTextAndStyle
} from "./drawUtils";
// @ts-ignore
import { parseDxfMTextContent } from "@dxfom/mtext";
import { Font } from "three/examples/jsm/loaders/FontLoader.js";
import { PickHelper } from "./pickHelper";
import { DrawRect } from "./drawRect";
import log from '@/utils/log/Logger';
let signal, middleObject;
let scene, helpScene, camera, renderer, inputElement, font, data;
// 渲染辉光图层
// let composer, finalComposer;
const BLOOM_SCENE = 10;
const bloomLayer = new THREE.Layers();
bloomLayer.set(BLOOM_SCENE);
// const DarkMaterial = new THREE.MeshBasicMaterial({ color: 'black' });
// const materials = {};
let modules: any = {};
// TODO 当前解析模式会存在 data.blocks 中还有实体但未匹配解析的情况;
// 记录已经解析的 data.blocks,data.entities遍历完成后处理
let parsedBlocks: string[] = [];
// 渲染配置项 && 对比度颜色实体集合
let options = {
bgColor: 0x000000,
contrastColor: 0xffffff,
fontUrl:''
}, contrastEntity: any = [];
// canvas默认宽高
export const state = {
width: 500,
height: 500,
};
/**
* 离屏canvas和普通canvas共用的绘制图纸方法
* 优先级:offScreenCanvas > canvas
*/
export function main(d) {
const { canvas, onComplete } = d;
signal = d.signal;
middleObject = d.middleObject;
state.width = canvas.width;
state.height = canvas.height;
inputElement = d.inputElement;
data = d.data;
// cad配置项
options = Object.assign(options, d.options);
options.bgColor = Number(options.bgColor);
options.contrastColor = options.bgColor === 0x000000 ? 0xffffff : 0x000000;
createLineTypeShaders();
renderer = new THREE.WebGLRenderer({
canvas,
powerPreference: "high-performance",
depth: false,
antialias: true,
});
// 第三个参数false代表不更新canvas dom style
renderer.setSize(canvas.width, canvas.height, false);
renderer.setClearColor(options.bgColor, 1);
renderer.toneMapping = THREE.ReinhardToneMapping;
// renderer.autoClear = false;
const loader = new TTFLoader();
loader.loadAsync(options.fontUrl).then(function (response) {
font = new Font(response);
init();
// OrbitControls mousemove事件中未调用chang事件,所以需要一直渲染
renderLoop();
// 添加灯光
scene.add(new THREE.AmbientLight(0xffffff, 1));
// 后期通道
// initComposer();
const controls = new OrbitControls(camera, inputElement);
controls.target.x = camera.position.x;
controls.target.y = camera.position.y;
controls.target.z = 0;
controls.mouseButtons = { LEFT: THREE.MOUSE.PAN, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.ROTATE };
controls.enableRotate = false;
controls.update();
// 注册模块
modules.drawRect = new DrawRect(inputElement, helpScene, camera, signal, middleObject);
modules.drawRect.setControls(controls);
const pickPosition = new THREE.Vector2(0, 0);
modules.pickHelper = new PickHelper(scene, camera, BLOOM_SCENE);
clearPickPosition();
function getCanvasRelativePosition(event) {
const rect = inputElement.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
}
function setPickPosition(event) {
const pos = getCanvasRelativePosition(event);
pickPosition.x = (pos.x / state.width) * 2 - 1;
pickPosition.y = (pos.y / state.height) * - 2 + 1;
modules.pickHelper.pick(pickPosition);
}
function clearPickPosition() {
//不像鼠标总是有一个位置,如果用户停止触摸屏幕,我们想要停止。现在我们只取一个不太可能取到的值
pickPosition.x = - 100000;
pickPosition.y = - 100000;
}
inputElement.addEventListener('mousemove', setPickPosition);
inputElement.addEventListener('mouseout', clearPickPosition);
inputElement.addEventListener('mouseleave', clearPickPosition);
inputElement.addEventListener('touchstart', (event) => {
// 防止窗口滚动
event.preventDefault();
setPickPosition(event.touches[0]);
}, { passive: false });
inputElement.addEventListener('touchmove', (event) => {
setPickPosition(event.touches[0]);
});
inputElement.addEventListener('touchend', clearPickPosition);
// 加载完毕
onComplete && onComplete();
font = undefined;
data = undefined;
resize({ width: inputElement.width, height: inputElement.height });
});
}
function createLineTypeShaders() {
let ltype, type;
if (!data.tables || !data.tables.lineType) return;
const ltypes = data.tables.lineType.lineTypes;
for (type in ltypes) {
ltype = ltypes[type];
if (!ltype.pattern) continue;
ltype.material = createDashedLineShader(ltype.pattern);
}
}
function createDashedLineShader(pattern) {
let dashedLineShader: any = {},
totalLength = 0.0;
for (let i = 0; i < pattern.length; i++) {
totalLength += Math.abs(pattern[i]);
}
dashedLineShader.uniforms = THREE.UniformsUtils.merge([
THREE.UniformsLib['common'],
THREE.UniformsLib['fog'],
{
// @ts-ignore
'pattern': { type: 'fv1', value: pattern },
// @ts-ignore
'patternLength': { type: 'f', value: totalLength }
}
]);
dashedLineShader.vertexShader = [
'attribute float lineDistance;',
'varying float vLineDistance;',
THREE.ShaderChunk['color_pars_vertex'],
'void main() {',
THREE.ShaderChunk['color_vertex'],
'vLineDistance = lineDistance;',
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
'}'
].join('\n');
dashedLineShader.fragmentShader = [
'uniform vec3 diffuse;',
'uniform float opacity;',
'uniform float pattern[' + pattern.length + '];',
'uniform float patternLength;',
'varying float vLineDistance;',
THREE.ShaderChunk['color_pars_fragment'],
THREE.ShaderChunk['fog_pars_fragment'],
'void main() {',
'float pos = mod(vLineDistance, patternLength);',
'for ( int i = 0; i < ' + pattern.length + '; i++ ) {',
'pos = pos - abs(pattern[i]);',
'if( pos < 0.0 ) {',
'if( pattern[i] > 0.0 ) {',
'gl_FragColor = vec4(1.0, 0.0, 0.0, opacity );',
'break;',
'}',
'discard;',
'}',
'}',
THREE.ShaderChunk['color_fragment'],
THREE.ShaderChunk['fog_fragment'],
'}'
].join('\n');
return dashedLineShader;
}
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(options.bgColor);
helpScene = new THREE.Scene();
const layersGroupMap: Map<string, THREE.Group> = new Map();
// 使用layers生成group
for (let layerName in data.tables.layer.layers) {
const layer = data.tables.layer.layers[layerName];
const group = new THREE.Group();
group.name = layerName;
group.visible = layer.visible;
layersGroupMap.set(layerName, group);
scene.add(group);
}
// 通过dxf数据创建场景
let entity, obj;
// 生成所有实体
for (let i = 0; i < data.entities?.length; i++) {
entity = data.entities[i];
obj = drawEntity(entity);
if (obj) {
if (layersGroupMap.has(entity.layer)) {
const parent = layersGroupMap.get(entity.layer) || scene;
parent.add(obj);
obj.name = `${entity.type}-${entity.handle || 'noHandle'}-${parent.children.length}`
} else {
scene.add(obj);
obj.name = `${entity.type}-${entity.handle || 'noHandle'}-${scene.children.length}`
}
}
obj = null;
}
// 2023-9-6:遍历所有剩下的未遍历的块,找出其中 INSERT/DIMENSION 类型的实体,进行解析.(非此类型就算解析位置也不正确)
// TODO 待删除
for (let blockName in data.blocks) {
const block = data.blocks[blockName];
if (parsedBlocks.includes(blockName) || !block.position || !block.entities) continue;
for (let j = 0; j < block.entities.length; j++) {
entity = block.entities[j];
if (entity.type == "INSERT" || entity.type == "DIMENSION") {
// TODO 容易栈溢出
// obj = this.drawEntity(entity);
//
// if (obj) {
// const bbox = new THREE.Box3().setFromObject(obj);
// if (isFinite(bbox.min.x) && (dims.min.x > bbox.min.x)) dims.min.x = bbox.min.x;
// if (isFinite(bbox.min.y) && (dims.min.y > bbox.min.y)) dims.min.y = bbox.min.y;
// if (isFinite(bbox.min.z) && (dims.min.z > bbox.min.z)) dims.min.z = bbox.min.z;
// if (isFinite(bbox.max.x) && (dims.max.x < bbox.max.x)) dims.max.x = bbox.max.x;
// if (isFinite(bbox.max.y) && (dims.max.y < bbox.max.y)) dims.max.y = bbox.max.y;
// if (isFinite(bbox.max.z) && (dims.max.z < bbox.max.z)) dims.max.z = bbox.max.z;
// this.scene.add(obj);
// }
// obj = null;
}
}
}
const aspectRatio = state.width / state.height;
const dims = new THREE.Box3().setFromObject(scene);
const upperRightCorner = { x: dims.max.x, y: dims.max.y };
const lowerLeftCorner = { x: dims.min.x, y: dims.min.y };
//const lowerLeftCorner = data.header.$EXTMIN; // X、Y 和 Z 图形范围左下角(在 WCS 中)
//const upperRightCorner = data.header.$EXTMAX; // X、Y 和 Z 图形范围右上角(在 WCS 中)
// 找出当前的视口范围
let vp_width = upperRightCorner.x - lowerLeftCorner.x;
let vp_height = upperRightCorner.y - lowerLeftCorner.y;
let center = {
x: vp_width / 2 + lowerLeftCorner.x,
y: vp_height / 2 + lowerLeftCorner.y
};
//let center = data.tables.viewPort.viewPorts[0].ucsOrigin;
// 将所有对象放入当前的查看器中
const extentsAspectRatio = Math.abs(vp_width / vp_height);
if (aspectRatio > extentsAspectRatio) {
vp_width = vp_height * aspectRatio;
} else {
vp_height = vp_width / aspectRatio;
}
const viewPort = {
bottom: -vp_height / 2,
left: -vp_width / 2,
top: vp_height / 2,
right: vp_width / 2,
center: {
x: center.x,
y: center.y
}
};
camera = new THREE.OrthographicCamera(viewPort.left, viewPort.right, viewPort.top, viewPort.bottom, 1, 19);
camera.position.z = 1;
camera.position.x = viewPort.center.x;
camera.position.y = viewPort.center.y;
camera.userData.viewPort = viewPort;
// 销毁中间变量
layersGroupMap.clear();
}
// function initComposer() {
// const pixelRatio = renderer.getPixelRatio();
//
// composer = new EffectComposer(renderer);
// composer.renderToScreen = false;
// composer.setPixelRatio(pixelRatio)
//
// let ssaaRenderPass = new SSAARenderPass(scene, camera);
// ssaaRenderPass.unbiased = false;
// ssaaRenderPass.sampleLevel = 2;
// composer.addPass(ssaaRenderPass);
//
// const bloomPass = new UnrealBloomPass(
// //参数一:泛光覆盖场景大小,二维向量类型
// new THREE.Vector2(inputElement.width, inputElement.height),
// //参数二:bloomStrength 泛光强度,值越大明亮的区域越亮,较暗区域变亮的范围越广
// 1.5,
// //参数三:bloomRadius 泛光散发半径
// 0,
// //参数四:bloomThreshold 泛光的光照强度阈值,如果照在物体上的光照强度大于该值就会产生泛光
// 0
// );
// composer.addPass(bloomPass);
//
// const mixPass = new ShaderPass(
// new THREE.ShaderMaterial({
// uniforms: {
// baseTexture: { value: null },
// bloomTexture: { value: composer.renderTarget2.texture }
// },
// vertexShader: `
// varying vec2 vUv;
// void main() {
// vUv = uv;
// gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
// }
// `,
// fragmentShader: `
// uniform sampler2D baseTexture;
// uniform sampler2D bloomTexture;
// varying vec2 vUv;
// void main() {
// gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );
// }`,
// defines: {}
// }), 'baseTexture'
// );
// mixPass.needsSwap = true;
//
// const outputPass = new OutputPass();
//
// finalComposer = new EffectComposer(renderer);
// finalComposer.addPass(ssaaRenderPass);
// finalComposer.addPass(mixPass);
// finalComposer.addPass(outputPass);
// }
// 图元选中
export function select({ modelOrName }) {
let model = modelOrName;
if (typeof modelOrName === 'string') {
model = scene.getObjectByProperty('name', modelOrName);
}
if (model) {
modules.pickHelper.select(model);
}
}
/** ------------------------------------------实体解析部分-------------------------------------------------------- **/
function getColor(entity) {
let color = options.contrastColor;//默认高对比度颜色
if (entity.color !== undefined) color = entity.color;
else if (data.tables && data.tables.layer && data.tables.layer.layers[entity.layer])
color = data.tables.layer.layers[entity.layer].color;
// 因为 scene.background = new THREE.Color(options.bgColor),所以黑色显示时改为白色
if (color == null || color === 0 || color === options.bgColor) {
color = options.contrastColor;
}
return color;
}
function drawEntity(entity) {
// 2023-9-6: 仅解析模型空间的实体
if (entity.inPaperSpace) return null;
let mesh;
switch (entity.type) {
case 'CIRCLE':
case 'ARC':
mesh = drawArc(entity);
break;
case 'LWPOLYLINE':
case 'LINE':
case 'POLYLINE':
mesh = drawLine(entity);
break;
case 'TEXT':
mesh = drawText(entity);
break;
case 'ATTDEF':
mesh = drawAttdef(entity);
break;
case 'ATTRIB':
mesh = drawAttrib(entity);
break;
case 'SOLID':
mesh = drawSolid(entity);
break;
case 'POINT':
mesh = drawPoint(entity);
break;
case 'INSERT':
mesh = drawBlock(entity);
break;
case 'SPLINE':
mesh = drawSpline(entity);
break;
case 'MTEXT':
// console.log("实体类型 MTEXT:", entity);
mesh = drawMtext(entity);
break;
case 'ELLIPSE':
mesh = drawEllipse(entity);
break;
case "HATCH":
mesh = drawHatch(entity);
break;
case 'DIMENSION':
const dimTypeEnum = entity.dimensionType & 7;
if (dimTypeEnum === 0) {
mesh = drawDimension(entity);
} else {
//console.log("不支持的维度类型: " + dimTypeEnum);
}
break;
default:
log.warn("不支持的实体类型: " + entity.type);
break;
}
if (mesh?.material && mesh.material?.color.getHex() === options.contrastColor) {
contrastEntity.push(mesh)
}
return mesh;
}
function drawArc(entity) {
let startAngle: number, endAngle: number;
if (entity.type === 'CIRCLE') {
startAngle = entity.startAngle || 0;
endAngle = startAngle + 2 * Math.PI;
} else {
startAngle = entity.startAngle;
endAngle = entity.endAngle;
}
//@ts-ignore
const curve = new THREE.ArcCurve(
0, 0,
entity.radius,
startAngle,
endAngle);
const points = curve.getPoints(32);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: getColor(entity) });
const arc = new THREE.Line(geometry, material);
arc.position.x = entity.center.x;
arc.position.y = entity.center.y;
arc.position.z = entity.center.z;
return arc;
}
function drawLine(entity) {
let points: THREE.Vector3[] = [];
let color = getColor(entity);
let material, lineType, vertex, startPoint, endPoint,
bulge, i, line;
if (!entity.vertices) return; //console.log('缺少顶点的实体.');
// 创建几何
for (i = 0; i < entity.vertices.length; i++) {
if (entity.vertices[i].bulge) {
bulge = entity.vertices[i].bulge;
startPoint = entity.vertices[i];
endPoint = i + 1 < entity.vertices.length ? entity.vertices[i + 1] : points[0];
let bulgePoints = getBulgeCurvePoints(startPoint, endPoint, bulge);
points.push.apply(points, bulgePoints);
} else {
vertex = entity.vertices[i];
points.push(new THREE.Vector3(vertex.x, vertex.y, 0));
}
}
if (entity.shape) points.push(points[0]);
// 设置材质
if (entity.lineType) {
lineType = data.tables.lineType.lineTypes[entity.lineType];
}
if (lineType && lineType.pattern && lineType.pattern.length !== 0) {
material = new THREE.LineDashedMaterial({ color: color, gapSize: 4, dashSize: 4 });
} else {
material = new THREE.LineBasicMaterial({ linewidth: 1, color: color });
}
const geometry = new THREE.BufferGeometry().setFromPoints(points);
line = new THREE.Line(geometry, material);
return line;
}
function drawText(entity) {
let geometry, material, text;
if (!font)
return log.warn('文本不支持没有Three.js字体加载的THREE.FontLoader!');
if (entity.text == null || entity.text == "") return null;
if (entity.textHeight == null || entity.textHeight == 0) return null;
// 当前使用的字体略宽,需要缩小一点
// xScale: 相对 X 比例因子 — 宽度
let fontSize = entity.xScale ? entity.xScale * entity.textHeight * 0.73 : entity.textHeight * 0.5;
geometry = new TextGeometry(entity.text, { font: font, depth: 0, size: fontSize || 100 });
if (entity.rotation) {
const zRotation = entity.rotation * Math.PI / 180;
geometry.rotateZ(zRotation);
}
material = new THREE.MeshBasicMaterial({ color: getColor(entity) });
text = new THREE.Mesh(geometry, material);
text.position.x = entity.startPoint.x;
text.position.y = entity.startPoint.y;
text.position.z = entity.startPoint.z;
return text;
}
function drawMtext(entity) {
const color = getColor(entity);
const textAndControlChars = parseDxfMTextContent(entity.text);
//Note: 目前只支持应用于所有mtext文本的单一格式
const content = mtextContentAndFormattingToTextAndStyle(textAndControlChars, entity, color);
/* 单行文本渲染模式 */
if (!font)
return log.warn('文本不支持没有Three.js字体加载的THREE.FontLoader!');
if (content.text == null || content.text == "") return null;
if (content.style.textHeight == null || content.style.textHeight == 0) return null;
const geometry = new TextGeometry(content.text, { font: font, depth: 0, size: content.style.textHeight || 100 });
if (entity.rotation) {
const zRotation = entity.rotation * Math.PI / 180;
geometry.rotateZ(zRotation);
}
if (entity.directionVector) {
const dv = entity.directionVector;
geometry.rotateZ(new THREE.Vector3(1, 0, 0).angleTo(new THREE.Vector3(dv.x, dv.y, dv.z)));
}
const material = new THREE.MeshBasicMaterial({ color });
const text = new THREE.Mesh(geometry, material);
text.position.x = entity.position.x;
text.position.y = entity.position.y;
text.position.z = entity.position.z;
return text;
/* TODO:多行文本渲染,拉近不显示 */
/*const txt = createTextForScene(content.text, content.style, entity, color,options.fontUrl);
if (!txt) return null;
console.log("MText txt:", txt);
// const group = new THREE.Object3D();
// group.add(txt);
return txt;*/
}
// @ts-ignore
function drawAttdef(entity) {
//console.log("drawAttdef:", entity);
}
// @ts-ignore
function drawAttrib(entity) {
//console.log("drawAttrib:", entity);
}
function drawSolid(entity) {
let material, verts,
geometry = new THREE.BufferGeometry();
const points = entity.points;
// verts = geometry.vertices;
verts = [];
addTriangleFacingCamera(verts, points[0], points[1], points[2]);
addTriangleFacingCamera(verts, points[1], points[2], points[3]);
material = new THREE.MeshBasicMaterial({ color: getColor(entity) });
geometry.setFromPoints(verts);
return new THREE.Mesh(geometry, material);
}
function drawPoint(entity) {
let geometry, material;
geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute([entity.position.x, entity.position.y, entity.position.z], 3));
const color = getColor(entity);
material = new THREE.PointsMaterial({ size: 0.1, color: new THREE.Color(color) });
return new THREE.Points(geometry, material);
// this.scene.add(point);
}
function drawBlock(entity) {
if (!data.blocks) return;
let block = data.blocks[entity.name];
if (!block) return null;
// 解析过的就存储起来,不再重复解析
parsedBlocks.push(entity.name);
if (!block.entities) return null;
const group = new THREE.Group();
if (entity.xScale) group.scale.x = entity.xScale;
if (entity.yScale) group.scale.y = entity.yScale;
if (entity.rotation) {
group.rotation.z = entity.rotation * Math.PI / 180;
}
if (entity.position) {
group.position.x = entity.position.x;
group.position.y = entity.position.y;
group.position.z = entity.position.z;
}
for (let i = 0; i < block.entities.length; i++) {
const childEntity = drawEntity(block.entities[i]);
if (childEntity) group.add(childEntity);
}
return group;
}
function drawSpline(entity) {
if (entity.controlPoints === undefined) return null;
const color = getColor(entity);
const points = getBSplinePolyline(entity.controlPoints, entity.degreeOfSplineCurve, entity.knotValues, 100, undefined);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ linewidth: 1, color: color });
return new THREE.Line(geometry, material);
}
function drawEllipse(entity) {
const color = getColor(entity);
const xrad = Math.sqrt(Math.pow(entity.majorAxisEndPoint.x, 2) + Math.pow(entity.majorAxisEndPoint.y, 2));
const yrad = xrad * entity.axisRatio;
const rotation = Math.atan2(entity.majorAxisEndPoint.y, entity.majorAxisEndPoint.x);
const curve = new THREE.EllipseCurve(
entity.center.x, entity.center.y,
xrad, yrad,
entity.startAngle, entity.endAngle,
false, // Always counterclockwise
rotation
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ linewidth: 1, color: color });
// 创建要添加到场景中的最后一个对象
return new THREE.Line(geometry, material);
}
function drawHatch(entity) {
if (entity.isSolid) return
// TODO 未实现
}
function drawDimension(entity) {
if (!data.blocks) return;
// console.log("drawDimension", entity.block)
const block = data.blocks[entity.block];
// 解析过的就存储起来,不再重复解析
parsedBlocks.push(entity.block);
if (!block || !block.entities) return null;
const group = new THREE.Group();
// if(entity.anchorPoint) {
// group.position.x = entity.anchorPoint.x;
// group.position.y = entity.anchorPoint.y;
// group.position.z = entity.anchorPoint.z;
// }
for (let i = 0; i < block.entities.length; i++) {
const childEntity = drawEntity(block.entities[i]);
if (childEntity) group.add(childEntity);
}
return group;
}
/** ------------------------------------------实体解析结束-------------------------------------------------------- **/
// 设置图层可见性
export function setLayerVisible(data: { layerName: string, visible: boolean }) {
const group = scene.getObjectByName(data.layerName);
if (group) group.visible = data.visible;
}
// 调用modules里的方法
export function callModuleMethod(data: { moduleName: string, methodName: string }) {
const module = modules[data.moduleName];
if (module && module[data.methodName]) {
module[data.methodName](data);
}
}
// 相机复位
export function resetCamera() {
const viewPort = camera.userData.viewPort;
if (!viewPort) return;
camera.left = viewPort.left;
camera.right = viewPort.right;
camera.top = viewPort.top;
camera.bottom = viewPort.bottom;
camera.position.z = 1;
camera.position.x = viewPort.center.x;
camera.position.y = viewPort.center.y;
modules.drawRect.controls.target.x = camera.position.x;
modules.drawRect.controls.target.y = camera.position.y;
modules.drawRect.controls.target.z = 0;
modules.drawRect.controls.object.zoom = 1;
modules.drawRect.controls.update();
}
//let start;
export function resize({ width, height }) {
if (!camera || data) return;
const hscale = width / state.width;
const vscale = height / state.height;
state.width = width;
state.height = height;
camera.top = (vscale * camera.top);
camera.bottom = (vscale * camera.bottom);
camera.left = (hscale * camera.left);
camera.right = (hscale * camera.right);
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
// composer.setSize(width, height);
// finalComposer.setSize(width, height);
render();
}
//
// function darkenNonBloomed(obj) {
// if (obj.material && bloomLayer.test(obj.layers) === false) {
// materials[obj.uuid] = obj.material;
// obj.material = DarkMaterial;
// }
// }
//
// function restoreMaterial(obj) {
// if (materials[obj.uuid]) {
// obj.material = materials[obj.uuid];
// delete materials[obj.uuid];
// }
// }
export function render() {
renderer.autoClear = false;
// scene.traverse(darkenNonBloomed);
// composer.render();
// scene.traverse(restoreMaterial);
// finalComposer.render();
renderer.render(scene, camera);
renderer.render(helpScene, camera);
renderer.autoClear = true;
}
export function stopRender() {
renderer.setAnimationLoop(null)
}
export function renderLoop() {
renderer.setAnimationLoop(render)
}
export function helpRender() {
renderer.autoClear = false;
renderer.render(helpScene, camera);
}
export function dispose() {
data = undefined;
font = undefined;
renderer.setAnimationLoop(null)
renderer?.dispose();
scene.children.forEach((obj) => {
scene.remove(obj);
})
camera.remove();
scene.remove();
}
/** ---------------------------- 设置弹窗配置变化 ---------------------------------- */
export function changeClearColor(color: 0x000000 | 0xffffff) {
options.bgColor = color;
options.contrastColor = options.bgColor === 0x000000 ? 0xffffff : 0x000000;
scene.background = new THREE.Color(options.bgColor);
contrastEntity.forEach(mesh => {
if (!mesh.material || !mesh.material.color) return;
mesh.material.color = new THREE.Color(options.contrastColor);
})
}
+281
View File
@@ -0,0 +1,281 @@
import {Vector2, Vector3} from "three";
import bSpline from './bspline';
import {Text} from "./troika-three-text.esm.js";
// Three.js 扩展函数
const THREEx: {
Math: {
angle2: (p1: { x: number, y: number }, p2: { x: number, y: number }) => number,
polar: (point: { x: number, y: number }, distance: number, angle: number) => { x: number, y: number },
}
} = {
Math: {
/**
* 返回向量(p1,p2)的弧度角。换句话说,想象把向量的底放在坐标(0,0)处,然后求向量(1,0)到(p1,p2)的夹角。
* @param {{x:number,y:number}} p1 起始点坐标
* @param {{x:number,y:number}} p2 结束点坐标
* @return {Number} the angle
*/
angle2: (p1: { x: number; y: number; }, p2: { x: number; y: number; }): number => {
const v1 = new Vector2(p1.x, p1.y);
const v2 = new Vector2(p2.x, p2.y);
v2.sub(v1); // sets v2 to be our chord
v2.normalize();
if (v2.y < 0) return -Math.acos(v2.x);
return Math.acos(v2.x);
},
polar: (point, distance, angle) => {
return {
x: point.x + distance * Math.cos(angle),
y: point.y + distance * Math.sin(angle)
};
}
}
}
/**
* 使用凸起值计算两点之间曲线的点。通常用于折线
* @param startPoint - 曲线的起点
* @param endPoint - 曲线的终点
* @param bulge - 凸起值:表示要弯曲多少的值
* @param {number} segments - 曲线的分段数
*/
export function getBulgeCurvePoints(startPoint: { x: number; y: number; }, endPoint: {
x: number | undefined;
y: number | undefined;
}, bulge: number, segments?: number | undefined) {
let vertex: { x: number, y: number }, i: number,
center: { x: number, y: number }, p0: Vector2, p1: Vector2, angle: number,
radius: number, startAngle: number,
thetaAngle: number;
p0 = startPoint ? new Vector2(startPoint.x, startPoint.y) : new Vector2(0, 0);
p1 = endPoint ? new Vector2(endPoint.x, endPoint.y) : new Vector2(1, 0);
bulge = bulge || 1;
const obj = {
startPoint: p0,
endPoint: p1,
bulge: bulge,
segments: segments || 100
};
angle = 4 * Math.atan(bulge);
radius = p0.distanceTo(p1) / 2 / Math.sin(angle / 2);
center = THREEx.Math.polar(startPoint, radius, THREEx.Math.angle2(p0, p1) + (Math.PI / 2 - angle / 2));
// 默认情况下,每10度需要一个段
obj.segments = segments = segments || Math.max(Math.abs(Math.ceil(angle / (Math.PI / 18))), 6);
startAngle = THREEx.Math.angle2(center, p0);
thetaAngle = angle / segments;
const vertices: Array<Vector3> = [];
vertices.push(new Vector3(p0.x, p0.y, 0));
for (i = 1; i <= segments - 1; i++) {
vertex = THREEx.Math.polar(center, Math.abs(radius), startAngle + thetaAngle * i);
vertices.push(new Vector3(vertex.x, vertex.y, 0));
}
return vertices;
}
/**
* 插值一条b样条。该算法检查结向量以创建分段进行插值。参数化值被重新归一化为[0,1],因为这是库所期望的(并且在b样条库中被反归一化)
* @param controlPoints the control points
* @param degree the b-spline degree
* @param knots the knot vector
* @returns the polyline
*/
export function getBSplinePolyline(controlPoints, degree, knots, interpolationsPerSplineSegment, weights) {
const polyline: Vector2[] = []
const controlPointsForLib = controlPoints.map(function (p) {
return [p.x, p.y]
})
const segmentTs = [knots[degree]]
const domain = [knots[degree], knots[knots.length - 1 - degree]]
for (let k = degree + 1; k < knots.length - degree; ++k) {
if (segmentTs[segmentTs.length - 1] !== knots[k]) {
segmentTs.push(knots[k])
}
}
interpolationsPerSplineSegment = interpolationsPerSplineSegment || 25
for (let i = 1; i < segmentTs.length; ++i) {
const uMin = segmentTs[i - 1]
const uMax = segmentTs[i]
for (let k = 0; k <= interpolationsPerSplineSegment; ++k) {
const u = k / interpolationsPerSplineSegment * (uMax - uMin) + uMin
// Clamp t to 0, 1 to handle numerical precision issues
let t = (u - domain[0]) / (domain[1] - domain[0])
t = Math.max(t, 0)
t = Math.min(t, 1)
const p = bSpline(t, degree, controlPointsForLib, knots, weights)
polyline.push(new Vector2(p[0], p[1]));
}
}
return polyline
}
export function addTriangleFacingCamera(verts, p0, p1, p2) {
// 计算这些点面对的方向(顺时针还是逆时针)
const vector1 = new Vector3();
const vector2 = new Vector3();
vector1.subVectors(p1, p0);
vector2.subVectors(p2, p0);
vector1.cross(vector2);
const v0 = new Vector3(p0.x, p0.y, p0.z);
const v1 = new Vector3(p1.x, p1.y, p1.z);
const v2 = new Vector3(p2.x, p2.y, p2.z);
// 如果z < 0,那么我们必须以相反的顺序来画
if (vector1.z < 0) {
verts.push(v2, v1, v0);
} else {
verts.push(v0, v1, v2);
}
}
export function mtextContentAndFormattingToTextAndStyle(textAndControlChars, entity, color) {
let activeStyle = {
horizontalAlignment: 'left',
textHeight: entity.height
}
const text: string[] = [];
for (let item of textAndControlChars) {
if (typeof item === 'string') {
if (item.startsWith('pxq') && item.endsWith(';')) {
if (item.indexOf('c') !== -1)
activeStyle.horizontalAlignment = 'center';
else if (item.indexOf('l') !== -1)
activeStyle.horizontalAlignment = 'left';
else if (item.indexOf('r') !== -1)
activeStyle.horizontalAlignment = 'right';
else if (item.indexOf('j') !== -1)
activeStyle.horizontalAlignment = 'justify';
} else {
text.push(item);
}
} else if (Array.isArray(item)) {
const nestedFormat = mtextContentAndFormattingToTextAndStyle(item, entity, color);
text.push(nestedFormat.text);
} else if (typeof item === 'object') {
if (item['S'] && item['S'].length === 3) {
text.push(item['S'][0] + '/' + item['S'][2]);
} else {
// not yet supported.
}
}
}
return {
text: text.join(),
style: activeStyle
}
}
export function createTextForScene(text, style, entity, color,fontUrl:string) {
if (!text) return null;
let textEnt:any = new Text();
textEnt.text = text.replaceAll('\\P', '\n').replaceAll('\\X', '\n');
textEnt.font = fontUrl;
textEnt.fontSize = style.textHeight;
textEnt.maxWidth = entity.width;
textEnt.position.x = entity.position.x;
textEnt.position.y = entity.position.y;
textEnt.position.z = entity.position.z;
textEnt.textAlign = style.horizontalAlignment;
textEnt.color = color;
if (entity.rotation) {
textEnt.rotation.z = entity.rotation * Math.PI / 180;
}
if (entity.directionVector) {
const dv = entity.directionVector;
textEnt.rotation.z = new Vector3(1, 0, 0).angleTo(new Vector3(dv.x, dv.y, dv.z));
}
switch (entity.attachmentPoint) {
case 1:
// Top Left
textEnt.anchorX = 'left';
textEnt.anchorY = 'top';
break;
case 2:
// Top Center
textEnt.anchorX = 'center';
textEnt.anchorY = 'top';
break;
case 3:
// Top Right
textEnt.anchorX = 'right';
textEnt.anchorY = 'top';
break;
case 4:
// Middle Left
textEnt.anchorX = 'left';
textEnt.anchorY = 'middle';
break;
case 5:
// Middle Center
textEnt.anchorX = 'center';
textEnt.anchorY = 'middle';
break;
case 6:
// Middle Right
textEnt.anchorX = 'right';
textEnt.anchorY = 'middle';
break;
case 7:
// Bottom Left
textEnt.anchorX = 'left';
textEnt.anchorY = 'bottom';
break;
case 8:
// Bottom Center
textEnt.anchorX = 'center';
textEnt.anchorY = 'bottom';
break;
case 9:
// Bottom Right
textEnt.anchorX = 'right';
textEnt.anchorY = 'bottom';
break;
default:
return undefined;
}
textEnt.sync(() => {
if (textEnt.textAlign !== 'left') {
textEnt.geometry.computeBoundingBox();
const textWidth = textEnt.geometry.boundingBox.max.x - textEnt.geometry.boundingBox.min.x;
if (textEnt.textAlign === 'center') textEnt.position.x += (entity.width - textWidth) / 2;
if (textEnt.textAlign === 'right') textEnt.position.x += (entity.width - textWidth);
}
});
return textEnt;
}
export function findExtents(scene) {
let minX;
let maxX;
let minY;
let maxY;
for (const child of scene.children) {
if (child.position) {
minX = Math.min(child.position.x, minX);
minY = Math.min(child.position.y, minY);
maxX = Math.max(child.position.x, maxX);
maxY = Math.max(child.position.y, maxY);
}
}
return {min: {x: minX, y: minY}, max: {x: maxX, y: maxY}};
}
+334
View File
@@ -0,0 +1,334 @@
import * as DrawShare from "./drawShare";
import {useAddSignal, useDispatchSignal, useRemoveSignal} from "@/hooks";
import OffScreenCanvasWorker from './offScreenCanvas.worker.ts?worker&url';
import App from "@/core/app/App.ts";
/** ----------------- 离屏渲染中的事件处理--------------------------- **/
const mouseEventHandler = makeSendPropertiesHandler( [
'ctrlKey',
'metaKey',
'shiftKey',
'button',
'pointerType',
'clientX',
'clientY',
'pageX',
'pageY',
] );
const wheelEventHandlerImpl = makeSendPropertiesHandler( [
'deltaX',
'deltaY',
] );
const keydownEventHandler = makeSendPropertiesHandler( [
'ctrlKey',
'metaKey',
'shiftKey',
'keyCode',
] );
function wheelEventHandler( event, sendFn ) {
event.preventDefault();
wheelEventHandlerImpl( event, sendFn );
}
function preventDefaultHandler( event ) {
event.preventDefault();
}
function copyProperties(src, properties, dst) {
for ( const name of properties ) {
dst[ name ] = src[ name ];
}
}
function makeSendPropertiesHandler( properties ) {
return function sendProperties(event, sendFn) {
const data = { type: event.type };
copyProperties( event, properties, data );
sendFn( data );
};
}
function touchEventHandler(event, sendFn) {
const touches: { pageX:number,pageY:number }[] = [];
const data = { type: event.type, touches };
for ( let i = 0; i < event.touches.length; ++ i ) {
const touch = event.touches[ i ];
touches.push({
pageX: touch.pageX,
pageY: touch.pageY,
});
}
sendFn( data );
}
// 四个方向键
const orbitKeys = {
'37': true, // left
'38': true, // up
'39': true, // right
'40': true, // down
};
function filteredKeydownEventHandler(event, sendFn) {
const { keyCode } = event;
if (orbitKeys[keyCode]) {
event.preventDefault();
keydownEventHandler( event, sendFn );
}
}
/** ----------------- 离屏渲染中的事件end --------------------------- **/
let nextProxyId = 0;
/**
* 转发 DOM 事件给Worker中的 ElementProxyReceiver
*/
class ElementProxy {
readonly id: number;
private worker: Worker;
private element: HTMLElement;
constructor(element, worker, eventHandlers) {
this.id = nextProxyId++;
this.worker = worker;
this.element = element;
// 注册一个响应元素id
worker.postMessage({
type: 'makeProxy',
data:{
id: this.id,
}
});
this.sendSize();
// 添加相应事件的监听
for (const [eventName, handler] of Object.entries(eventHandlers)) {
element.addEventListener(eventName, (event) => {
// @ts-ignore
handler(event, this.sendEvent.bind(this));
});
}
}
sendEvent(data){
this.worker.postMessage({
type: 'event',
data:{
id: this.id,
data,
}
});
};
sendSize() {
const rect = this.element.getBoundingClientRect();
this.sendEvent({
type: 'size',
left: rect.left,
top: rect.top,
width: this.element.clientWidth,
height: this.element.clientHeight,
});
}
}
let cadDialogMoveFn;
/**
* dxf对象的查看器类
* @param {any} data - dxf对象
* @param {HTMLCanvasElement} canvas - three 画布
* @param {Number} width - 渲染画布的宽度,以像素为单位
* @param {Number} height - 渲染画布的高度,以像素为单位
* @constructor
*/
class DxfViewer {
private worker:Worker | undefined;
private resizeObserver: ResizeObserver;
private proxy: ElementProxy | undefined;
private middleObject:any = new Proxy({},{
set(target, p: string | symbol, newValue: any): boolean {
target[p] = newValue;
// switch (p) {
// case "markList":
// break;
// }
return true;
}
})
constructor(data: any, canvas: HTMLCanvasElement, width: number, height: number, onComplete?:()=>void) {
//console.log('dxf data:', data);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const cadFontUrl = new URL(import.meta.env.BASE_URL + 'resource/fonts/Alimama_DongFangDaKai_Regular.ttf', import.meta.url).href;
// 检查是否支持离屏canvas
if (canvas.transferControlToOffscreen) {
// console.log('OffscreenCanvas supported');
// 创建Worker进行离屏渲染
this.worker = new Worker(OffScreenCanvasWorker, {type: 'module'});
const offscreen = canvas.transferControlToOffscreen();
offscreen.width = width;
offscreen.height = height;
// 添加鼠标事件的监听
const eventHandlers = {
contextmenu: preventDefaultHandler,
mousedown: mouseEventHandler,
mousemove: mouseEventHandler,
mouseup: mouseEventHandler,
pointerdown: mouseEventHandler,
pointermove: mouseEventHandler,
pointerup: mouseEventHandler,
touchstart: touchEventHandler,
//touchmove: touchEventHandler,
touchend: touchEventHandler,
wheel: wheelEventHandler,
keydown: filteredKeydownEventHandler,
};
this.proxy = new ElementProxy(canvas, this.worker, eventHandlers);
this.worker.postMessage({
type: 'start', data: {
canvas: offscreen,
canvasId: this.proxy.id,
data:data,
options: {
bgColor:0x000000,
contrastColor:0xffffff,
fontUrl:cadFontUrl
},
middleObject:{
markList: JSON.stringify(App.project.getKey("drawing.markList")),
selectedRectIndex: App.project.getKey("drawing.selectedRectIndex"),
}
}
}, [offscreen]);
this.worker.onmessage = (event) => {
const data = event.data;
switch (data.type) {
case 'complete':
onComplete && onComplete();
break;
case "signal":
this.handleSignal(data.data)
break;
case "style":
canvas.style[data.data.key] = data.data.value;
break;
case "middle":
this.middleObject[data.data.key] = data.data.value;
break;
}
}
cadDialogMoveFn = this.proxy.sendSize.bind(this.proxy);
useAddSignal("cadViewerResize",cadDialogMoveFn)
}else{
canvas.width = width;
canvas.height = height;
//不支持离屏渲染
DrawShare.main({
canvas,
inputElement:canvas,
data:data,
onComplete:onComplete,
signal:this.handleSignal,
options: {
bgColor:0x000000,
contrastColor:0xffffff,
fontUrl:cadFontUrl
},
middleObject:this.middleObject
});
}
// 监听窗口变化
this.resizeObserver = new ResizeObserver(entries => {
canvas.style.width = `${entries[0].contentRect.width}px`;
canvas.style.height = `${entries[0].contentRect.height}px`;
const data = {
width:entries[0].contentRect.width,
height:entries[0].contentRect.height
}
if(this.worker){
this.worker.postMessage({
type:"resize",
data
})
this.proxy?.sendSize();
}else{
DrawShare.resize(data)
}
});
this.resizeObserver.observe(canvas.parentElement as HTMLDivElement);
}
// 触发signal
handleSignal(args){
const {type,name,data} = args;
switch (type) {
case "dispatch":
useDispatchSignal(name,...data)
break;
}
}
// 调用drawShare中的方法,data为传入的参数,对象展示
callMethod(methodName:string, data:any = {}){
if(this.worker){
this.worker.postMessage({
type:methodName,
data,
})
}else{
DrawShare[methodName](data);
}
}
dispose() {
if(this.worker){
this.worker.terminate();
useRemoveSignal("cadViewerResize",cadDialogMoveFn)
}else{
DrawShare.dispose();
}
this.resizeObserver.disconnect();
}
/* ------------------- 需要共同实现的标记相关方法 ------------------------- */
// 获取选中的标记
get selectRectIndex(){
return this.middleObject.selectRectIndex;
}
// 删除选中的标记
deleteRect(){
const id = this.middleObject.selectRect?.id;
if(!id) return;
const elementId = this.middleObject.selectRect?.elementId;
this.callMethod('callModuleMethod',{
moduleName: "drawRect", methodName: "deleteRect",elementId:elementId
})
}
// 根据模型选中对应标记
selectRect(uuid:string){
this.callMethod('callModuleMethod',{
moduleName: "drawRect", methodName: "setSelect",elementId:uuid
})
}
}
export {DxfViewer}
@@ -0,0 +1,192 @@
import {EventDispatcher} from "three";
import * as DrawShare from "./drawShare";
function noop() {
}
class ElementProxyReceiver extends EventDispatcher {
// @ts-ignore
private style: {};
// @ts-ignore
private width: number;
// @ts-ignore
private height: number;
// @ts-ignore
private left: number;
// @ts-ignore
private top: number;
constructor() {
super();
// 因为OrbitControls尝试设置style.touchAction;
this.style = new Proxy({}, {
set(target: {}, p: string | symbol, newValue: any): boolean {
self.postMessage({
type: 'style',
data: {
key: p,
value: newValue
}
});
target[p] = newValue;
return true;
}
});
}
get clientWidth() {
return this.width;
}
get clientHeight() {
return this.height;
}
get offsetLeft() {
return this.left;
}
get offsetTop() {
return this.top;
}
// r132 版本的 OrbitControls 会调用这两个函数,也许我们应该实现一下
setPointerCapture() {
}
releasePointerCapture() {
}
getBoundingClientRect() {
return {
left: this.left,
top: this.top,
width: this.width,
height: this.height,
right: this.left + this.width,
bottom: this.top + this.height,
};
}
// 触发事件
handleEvent(data) {
if (data.type === 'size') {
this.left = data.left;
this.top = data.top;
this.width = data.width;
this.height = data.height;
return;
}
data.preventDefault = noop;
data.stopPropagation = noop;
// @ts-ignore
this.dispatchEvent(data);
}
focus() {
// no-op
}
// ----------------------- 2024/04/12 r163 版本 OrbitControls 新增的函数调用 ------------------------------
getRootNode(){
return this;
}
}
class ProxyManager {
private readonly targets: {};
constructor() {
this.targets = {};
this.handleEvent = this.handleEvent.bind(this);
}
/**
* 通过一个id,可以生成一个响应对应id信息的 ElementProxyReceiver 对象。
* @param data
*/
makeProxy(data: { id: number }) {
const {id} = data;
this.targets[id] = new ElementProxyReceiver();
}
getProxy(id: number) {
return this.targets[id];
}
handleEvent(data) {
this.targets[data.id].handleEvent(data.data);
}
}
/** ---------------- worker主体 ------------------ **/
const proxyManager = new ProxyManager();
const middleObject:any = new Proxy({}, {
set(target: {}, p: string | symbol, newValue: any): boolean {
self.postMessage({
type: 'middle',
data: {
key: p,
value: newValue
}
});
target[p] = newValue;
return true;
}
});
function start(data: { canvasId: number, canvas: HTMLCanvasElement, data: any,options,middleObject }) {
const proxy = proxyManager.getProxy(data.canvasId);
middleObject.markList = JSON.parse(data.middleObject.markList);
middleObject.selectedRectIndex = data.middleObject.selectedRectIndex;
DrawShare.main({
canvas: data.canvas,
inputElement: proxy,
data: data.data,
options:data.options,
onComplete: () => {
self.postMessage({
type: 'complete'
});
},
signal: (args: { type: 'add' | 'remove' | 'dispatch', name: string, data?: any }) => {
self.postMessage({
type: 'signal',
data: args
});
},
middleObject
});
}
function makeProxy(data: { id: number }) {
proxyManager.makeProxy(data);
}
const handlers = {
start,
makeProxy,
event: proxyManager.handleEvent,
};
self.onmessage = function ({data}) {
const fn = handlers[data.type] || DrawShare[data.type];
if (typeof fn !== 'function') {
throw new Error(`没有 ${data.type} 类型的处理程序!`);
}
fn(data.data);
};
@@ -0,0 +1,265 @@
/**
* AutoCad files sometimes use an indexed color value between 1 and 255 inclusive.
* Each value corresponds to a color. index 1 is red, that is 16711680 or 0xFF0000.
* index 0 and 256, while included in this array, are actually reserved for inheritance
* values in AutoCad so they should not be used for index color lookups.
*/
export default [
0,
16711680,
16776960,
65280,
65535,
255,
16711935,
16777215,
8421504,
12632256,
16711680,
16744319,
13369344,
13395558,
10027008,
10046540,
8323072,
8339263,
4980736,
4990502,
16727808,
16752511,
13382400,
13401958,
10036736,
10051404,
8331008,
8343359,
4985600,
4992806,
16744192,
16760703,
13395456,
13408614,
10046464,
10056268,
8339200,
8347455,
4990464,
4995366,
16760576,
16768895,
13408512,
13415014,
10056192,
10061132,
8347392,
8351551,
4995328,
4997670,
16776960,
16777087,
13421568,
13421670,
10000384,
10000460,
8355584,
8355647,
5000192,
5000230,
12582656,
14679935,
10079232,
11717734,
7510016,
8755276,
6258432,
7307071,
3755008,
4344870,
8388352,
12582783,
6736896,
10079334,
5019648,
7510092,
4161280,
6258495,
2509824,
3755046,
4194048,
10485631,
3394560,
8375398,
2529280,
6264908,
2064128,
5209919,
1264640,
3099686,
65280,
8388479,
52224,
6736998,
38912,
5019724,
32512,
4161343,
19456,
2509862,
65343,
8388511,
52275,
6737023,
38950,
5019743,
32543,
4161359,
19475,
2509871,
65407,
8388543,
52326,
6737049,
38988,
5019762,
32575,
4161375,
19494,
2509881,
65471,
8388575,
52377,
6737074,
39026,
5019781,
32607,
4161391,
19513,
2509890,
65535,
8388607,
52428,
6737100,
39064,
5019800,
32639,
4161407,
19532,
2509900,
49151,
8380415,
39372,
6730444,
29336,
5014936,
24447,
4157311,
14668,
2507340,
32767,
8372223,
26316,
6724044,
19608,
5010072,
16255,
4153215,
9804,
2505036,
16383,
8364031,
13260,
6717388,
9880,
5005208,
8063,
4149119,
4940,
2502476,
255,
8355839,
204,
6710988,
152,
5000344,
127,
4145023,
76,
2500172,
4129023,
10452991,
3342540,
8349388,
2490520,
6245528,
2031743,
5193599,
1245260,
3089996,
8323327,
12550143,
6684876,
10053324,
4980888,
7490712,
4128895,
6242175,
2490444,
3745356,
12517631,
14647295,
10027212,
11691724,
7471256,
8735896,
6226047,
7290751,
3735628,
4335180,
16711935,
16744447,
13369548,
13395660,
9961624,
9981080,
8323199,
8339327,
4980812,
4990540,
16711871,
16744415,
13369497,
13395634,
9961586,
9981061,
8323167,
8339311,
4980793,
4990530,
16711807,
16744383,
13369446,
13395609,
9961548,
9981042,
8323135,
8339295,
4980774,
4990521,
16711743,
16744351,
13369395,
13395583,
9961510,
9981023,
8323103,
8339279,
4980755,
4990511,
3355443,
5987163,
8684676,
11382189,
14079702,
16777215
];
@@ -0,0 +1,31 @@
// 维度样式变量可以在DIMSTYLE表中使用,也可以在XDATA中的Dimension实体样式覆盖中使用。
const codes = new Map([
[140, "DIMTXT"],
[142, "DIMTSZ"],
[144, "DIMLFAC"],
[147, "DIMGAP"],
[173, "DIMSAH"],
[175, "DIMSOXD"],
[176, "DIMCLRD"],
[177, "DIMCLRE"],
[178, "DIMCLRT"],
[271, "DIMDEC"],
[278 ,"DIMDSEP"],
[281, "DIMSD1"],
[282, "DIMSD2"],
[3, "DIMPOST"],
[40, "DIMSCALE"],
[41, "DIMASZ"],
[42, "DIMEXO"],
[44, "DIMEXE"],
[45, "DIMRND"],
[46, "DIMDLE"],
[5, "DIMBLK"],
[6, "DIMBLK1"],
[7, "DIMBLK2"],
[75, "DIMSE1"],
[76, "DIMSE2"],
[78, "DIMZIN"],
])
export default codes
@@ -0,0 +1,168 @@
import log from '@/utils/log/Logger';
export interface IGroup {
code: number;
value: number | string | boolean;
}
/**
* DxfArrayScanner
*
* 基于AutoCAD 2012 DXF参考
* http://images.autodesk.com/adsk/files/autocad_2012_pdf_dxf-reference_enu.pdf
*
* 读取表示dxf文件行的数组。接受一个数组,并提供一个简单的接口来提取组代码和值对。
* @param data - 一个数组,其中每个元素表示DXF文件中的一行
* @constructor
*/
export default class DxfArrayScanner {
private _pointer = 0;
private _eof = false;
public lastReadGroup: IGroup | undefined;
private _data: string[];
constructor(data: string[]) {
this._data = data;
// console.log('this._data:', this._data);
}
/**
* 从数组中获取下一组(代码、值)。组是数组中两个连续的元素。第一个是代码,第二个是值。
* @returns {{code: Number}|*}
*/
public next() {
if (!this.hasNext()) {
// console.log('this._eof:', this._eof);
// console.log('this._pointer:', this._pointer);
if (!this._eof){
log.error('输入结束异常:EOF组未在文件结束前读取。代码结束: ' + this._data[this._pointer]);
}else{
log.error('在读取完 EOF 组后无法调用 `next`');
}
this._eof = true;
return { code: 0, value: 'EOF' };
}
const group = {
code: parseInt(this._data[this._pointer])
} as IGroup;
if(isNaN(group.code)){
// 回退code: 如果当前code为NaN,很可能是因为当前行和上一行是连贯数据,被libreDWG解析为两行,所以要回退code采用同一个code解析
this._pointer--;
group.code = parseInt(this._data[this._pointer - 1]);
// TODO: 或者使用跳过当前行操作?
// this._pointer++;
// return this.next();
}
this._pointer++;
group.value = parseGroupValue(group.code, this._data[this._pointer].trim());
this._pointer++;
// if (group.code === 0 && (group.value === 'EOF' || (group.value === 'ENDSEC' && !this.hasNext()))) this._eof = true;
if (group.code === 0 && group.value === 'EOF') this._eof = true;
this.lastReadGroup = group;
return group;
}
public peek() {
if (!this.hasNext()) {
if (!this._eof)
throw new Error('输入结束异常:EOF组未在文件结束前读取。代码结束: ' + this._data[this._pointer]);
else
throw new Error('Cannot call \'next\' after EOF group has been read');
}
const group = {
code: parseInt(this._data[this._pointer])
} as IGroup;
group.value = parseGroupValue(group.code, this._data[this._pointer + 1].trim());
return group;
}
public rewind(numberOfGroups = 1) {
this._pointer = this._pointer - numberOfGroups * 2;
}
/**
* 如果存在另一个代码/值对(数组中的2个元素)则返回true。
* @returns {boolean}
*/
public hasNext() {
// 检查我们是否读过EOF组代码
if (this._eof) {
return false;
}
// 我们需要确保有两条线路可用
return this._pointer <= this._data.length - 2;
}
/**
* 如果扫描器位于数组的末尾,则返回true
* @returns {boolean}
*/
public isEOF() {
return this._eof;
}
}
/**
* 将值解析为其适当类型。
* 参见本文件顶部提供的AutoCAD DXF 2012参考的第3-10页。
*
* @param code
* @param value
* @returns {*}
*/
function parseGroupValue(code: number, value: string) {
if (code <= 9) return value;
if (code >= 10 && code <= 59) return parseFloat(value);
if (code >= 60 && code <= 99) return parseInt(value);
if (code >= 100 && code <= 109) return value;
if (code >= 110 && code <= 149) return parseFloat(value);
if (code >= 160 && code <= 179) return parseInt(value);
if (code >= 210 && code <= 239) return parseFloat(value);
if (code >= 270 && code <= 289) return parseInt(value);
if (code >= 290 && code <= 299) return parseBoolean(value as '0' | '1');
if (code >= 300 && code <= 369) return value;
if (code >= 370 && code <= 389) return parseInt(value);
if (code >= 390 && code <= 399) return value;
if (code >= 400 && code <= 409) return parseInt(value);
if (code >= 410 && code <= 419) return value;
if (code >= 420 && code <= 429) return parseInt(value);
if (code >= 430 && code <= 439) return value;
if (code >= 440 && code <= 459) return parseInt(value);
if (code >= 460 && code <= 469) return parseFloat(value);
if (code >= 470 && code <= 481) return value;
if (code === 999) return value;
if (code >= 1000 && code <= 1009) return value;
if (code >= 1010 && code <= 1059) return parseFloat(value);
if (code >= 1060 && code <= 1071) return parseInt(value);
//log.warn('group代码没有定义的类型: %j', { code: code, value: value });
return value;
}
/**
* 根据1或0值解析布尔值
* @param str
* @returns {boolean}
*/
function parseBoolean(str: '0' | '1') {
if (str === '0') return false;
if (str === '1') return true;
return str;
// throw TypeError('String \'' + str + '\' cannot be cast to Boolean type');
}
+980
View File
@@ -0,0 +1,980 @@
import {Readable} from 'stream';
import DxfArrayScanner, {IGroup} from './DxfArrayScanner';
import AUTO_CAD_COLOR_INDEX from './AutoCadColorIndex';
import dimStyleCodes from './DimStyleCodes';
import Face from './entities/3dface';
import Arc from './entities/arc';
import AttDef from './entities/attdef';
import Attribute from './entities/attribute'
import Circle from './entities/circle';
import Dimension from './entities/dimension';
import Ellipse from './entities/ellipse';
import Hatch from './entities/hatch';
import Insert from './entities/insert';
import Line from './entities/line';
import LWPolyline from './entities/lwpolyline';
import MText from './entities/mtext';
import Point from './entities/point';
import Polyline from './entities/polyline';
import Solid from './entities/solid';
import Spline from './entities/spline';
import Text from './entities/text';
import Vertex from './entities/vertex';
import log from '@/utils/log/Logger';
import IGeometry, {EntityName, IEntity, IPoint} from './entities/geomtry';
export interface IBlock {
entities: IEntity[];
type: number;
ownerHandle: string;
// entities: any[];
xrefPath: string;
name: string;
name2: string;
handle: string;
layer: string;
position: IPoint;
paperSpace: boolean;
}
export interface IViewPort {
name: string;
lowerLeftCorner: IPoint;
upperRightCorner: IPoint;
center: IPoint;
snapBasePoint: IPoint;
snapSpacing: IPoint;
gridSpacing: IPoint;
viewDirectionFromTarget: IPoint;
viewTarget: IPoint;
lensLength: number;
frontClippingPlane: string | number | boolean;
backClippingPlane: string | number | boolean;
viewHeight: number;
snapRotationAngle: number;
viewTwistAngle: number;
orthographicType: string;
ucsOrigin: IPoint;
ucsXAxis: IPoint;
ucsYAxis: IPoint;
renderMode: string;
defaultLightingType: string;
defaultLightingOn: string;
ownerHandle: string;
ambientColor: number;
viewMode: string;
}
export interface IViewPortTableDefinition {
tableRecordsProperty: 'viewPorts';
tableName: 'viewPort';
dxfSymbolName: 'VPORT';
parseTableRecords(): IViewPort[];
}
export interface ILineType {
name: string;
description: string;
pattern: string[];
patternLength: number;
}
export interface ILineTypeTableDefinition {
tableRecordsProperty: 'lineTypes';
tableName: 'lineType';
dxfSymbolName: 'LTYPE';
parseTableRecords(): Record<string, ILineType>;
}
export interface ILayer {
name: string;
visible: boolean;
colorIndex: number;
color: number;
frozen: boolean;
}
export interface ILayerTableDefinition {
tableRecordsProperty: 'layers';
tableName: 'layer';
dxfSymbolName: 'LAYER';
parseTableRecords(): Record<string, ILayer>;
}
export interface ITableDefinitions {
VPORT: IViewPortTableDefinition;
LTYPE: ILineTypeTableDefinition;
LAYER: ILayerTableDefinition;
DIMSTYLE: any;
STYLE: any;
}
export interface IBaseTable {
handle: string;
ownerHandle: string;
}
export interface IViewPortTable extends IBaseTable {
viewPorts: IViewPort[];
}
export interface ILayerTypesTable extends IBaseTable {
lineTypes: Record<string, ILineType>;
}
export interface ILayersTable extends IBaseTable {
layers: Record<string, ILayer>;
}
export interface ITables {
viewPort: IViewPortTable;
lineType: ILayerTypesTable;
layer: ILayersTable;
}
export type ITable = IViewPortTable | ILayerTypesTable | ILayersTable;
export interface IDxf {
header: Record<string, IPoint | number>;
entities: IEntity[];
blocks: Record<string, IBlock>;
tables: ITables;
}
function registerDefaultEntityHandlers(dxfParser: DxfParser) {
// 这里支持的实体(一些实体代码仍然被重构到这个流中)
dxfParser.registerEntityHandler(Face);
dxfParser.registerEntityHandler(Arc);
dxfParser.registerEntityHandler(AttDef);
dxfParser.registerEntityHandler(Attribute);
dxfParser.registerEntityHandler(Circle);
dxfParser.registerEntityHandler(Dimension);
dxfParser.registerEntityHandler(Ellipse);
dxfParser.registerEntityHandler(Hatch);
dxfParser.registerEntityHandler(Insert);
dxfParser.registerEntityHandler(Line);
dxfParser.registerEntityHandler(LWPolyline);
dxfParser.registerEntityHandler(MText);
dxfParser.registerEntityHandler(Point);
dxfParser.registerEntityHandler(Polyline);
dxfParser.registerEntityHandler(Solid);
dxfParser.registerEntityHandler(Spline);
dxfParser.registerEntityHandler(Text);
dxfParser.registerEntityHandler(Vertex);
}
export default class DxfParser {
private _entityHandlers = {} as Record<EntityName, IGeometry>;
constructor() {
registerDefaultEntityHandlers(this);
}
public parse(source: string | Readable) {
if (typeof source === 'string') {
return this._parse(source);
} else {
log.error('无法读取dxf源的类型: `' + typeof (source));
return null;
}
}
public registerEntityHandler(handlerType: new () => IGeometry) {
const instance = new handlerType();
this._entityHandlers[instance.ForEntityName] = instance;
}
public parseSync(source: string) {
return this.parse(source);
}
public parseStream(stream: Readable) {
let dxfString = "";
const self = this;
return new Promise<IDxf>((res, rej) => {
stream.on('data', (chunk) => {
dxfString += chunk;
});
stream.on('end', () => {
try {
res(self._parse(dxfString));
} catch (err) {
rej(err);
}
});
stream.on('error', (err) => {
rej(err);
});
});
}
private _parse(dxfString: string) {
const dxf = {} as IDxf;
let lastHandle = 0;
const dxfLinesArray = dxfString.split(/\r\n|\r|\n/g);
const scanner = new DxfArrayScanner(dxfLinesArray);
if (!scanner.hasNext()) {
log.error('当前 CAD 文件为空文件');
return dxf;
}
const self = this;
let curr: IGroup;
function parseAll() {
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0 && curr.value === 'SECTION') {
curr = scanner.next();
// 确保我们读的是段代码
if (curr.code !== 2) {
log.error(`Unexpected code ${debugCode(curr) } after 0:SECTION`);
curr = scanner.next();
continue;
}
if (curr.value === 'HEADER') {
dxf.header = parseHeader();
} else if (curr.value === 'BLOCKS') {
dxf.blocks = parseBlocks();
} else if (curr.value === 'ENTITIES') {
dxf.entities = parseEntities(false);
} else if (curr.value === 'TABLES') {
dxf.tables = parseTables();
} else if (curr.value === 'EOF') {
log.debug('EOF');
} else {
log.warn('Skipping section ' + curr.value);
}
} else {
curr = scanner.next();
}
// If is a new section
}
}
/**
*
* @return {object} header
*/
function parseHeader() {
// interesting variables:
// $ACADVER, $VIEWDIR, $VIEWSIZE, $VIEWCTR, $TDCREATE, $TDUPDATE
// http://www.autodesk.com/techpubs/autocad/acadr14/dxf/header_section_al_u05_c.htm
// Also see VPORT table entries
let currVarName = null as null | string;
let currVarValue = null as null | IPoint | number;
const header = {} as Record<string, IPoint | number>;
// loop through header variables
curr = scanner.next();
while (true) {
if (groupIs(curr, 0, 'ENDSEC')) {
if (currVarName) header[currVarName as string] = currVarValue!;
break;
} else if (curr.code === 9) {
if (currVarName) header[currVarName as string] = currVarValue!;
currVarName = curr.value as string;
// Filter here for particular variables we are interested in
} else {
if (curr.code === 10) {
currVarValue = {x: curr.value as number} as IPoint;
} else if (curr.code === 20) {
(currVarValue as IPoint).y = curr.value as number;
} else if (curr.code === 30) {
(currVarValue as IPoint).z = curr.value as number;
} else {
currVarValue = curr.value as number;
}
}
curr = scanner.next();
}
// console.log(util.inspect(header, { colors: true, depth: null }));
curr = scanner.next(); // swallow up ENDSEC
return header;
}
function parseBlocks() {
const blocks = {} as Record<string, IBlock>;
curr = scanner.next();
while (curr.value !== 'EOF') {
if (groupIs(curr, 0, 'ENDSEC')) {
break;
}
if (groupIs(curr, 0, 'BLOCK')) {
const block = parseBlock();
ensureHandle(block);
if (!block.name) {
// log.error('block with handle "' + block.handle + '" is missing a name.');
// block.name = 'InvalidBlockName-' + block.ownerHandle;
}else{
blocks[block.name] = block;
}
} else {
logUnhandledGroup(curr);
curr = scanner.next();
}
}
return blocks;
}
function parseBlock() {
const block = {} as IBlock;
curr = scanner.next();
while (curr.value !== 'EOF') {
switch (curr.code) {
case 1:
block.xrefPath = curr.value as string;
curr = scanner.next();
break;
case 2:
block.name = curr.value as string;
curr = scanner.next();
break;
case 3:
block.name2 = curr.value as string;
curr = scanner.next();
break;
case 5:
block.handle = curr.value as string;
curr = scanner.next();
break;
case 8:
block.layer = curr.value as string;
curr = scanner.next();
break;
case 10:
block.position = parsePoint(curr);
curr = scanner.next();
break;
case 67:
block.paperSpace = (curr.value && curr.value == 1) ? true : false;
curr = scanner.next();
break;
case 70:
if (curr.value != 0) {
//if(curr.value & BLOCK_ANONYMOUS_FLAG) console.log(' Anonymous block');
//if(curr.value & BLOCK_NON_CONSTANT_FLAG) console.log(' Non-constant attributes');
//if(curr.value & BLOCK_XREF_FLAG) console.log(' Is xref');
//if(curr.value & BLOCK_XREF_OVERLAY_FLAG) console.log(' Is xref overlay');
//if(curr.value & BLOCK_EXTERNALLY_DEPENDENT_FLAG) console.log(' Is externally dependent');
//if(curr.value & BLOCK_RESOLVED_OR_DEPENDENT_FLAG) console.log(' Is resolved xref or dependent of an xref');
//if(curr.value & BLOCK_REFERENCED_XREF) console.log(' This definition is a referenced xref');
block.type = curr.value as number;
}
curr = scanner.next();
break;
case 100:
// ignore class markers
curr = scanner.next();
break;
case 330:
block.ownerHandle = curr.value as string;
curr = scanner.next();
break;
case 0:
if (curr.value == 'ENDBLK')
break;
block.entities = parseEntities(true);
break;
default:
logUnhandledGroup(curr);
curr = scanner.next();
}
if (groupIs(curr, 0, 'ENDBLK')) {
curr = scanner.next();
break;
}
}
return block;
}
/**
* parseTables
* @return {Object} Object representing tables
*/
function parseTables() {
const tables = {} as ITables;
curr = scanner.next();
while (curr.value !== 'EOF') {
if (groupIs(curr, 0, 'ENDSEC'))
break;
if (groupIs(curr, 0, 'TABLE')) {
curr = scanner.next();
const tableDefinition = tableDefinitions[curr.value as keyof ITableDefinitions];
if (tableDefinition) {
tables[tableDefinitions[curr.value as keyof ITableDefinitions].tableName] = parseTable(curr);
} else {
// log.debug('Unhandled Table ' + curr.value);
}
} else {
// else ignored
curr = scanner.next();
}
}
curr = scanner.next();
return tables;
}
const END_OF_TABLE_VALUE = 'ENDTAB';
function parseTable<T extends IBaseTable = ITable>(group: IGroup) {
const tableDefinition = tableDefinitions[group.value as keyof ITableDefinitions];
const table = {} as T;
let expectedCount = 0;
curr = scanner.next();
while (!groupIs(curr, 0, END_OF_TABLE_VALUE)) {
switch (curr.code) {
case 5:
table.handle = curr.value as string;
curr = scanner.next();
break;
case 330:
table.ownerHandle = curr.value as string;
curr = scanner.next();
break;
case 100:
if (curr.value === 'AcDbSymbolTable') {
// ignore
curr = scanner.next();
} else {
logUnhandledGroup(curr);
curr = scanner.next();
}
break;
case 70:
expectedCount = curr.value as number;
curr = scanner.next();
break;
case 0:
if (curr.value === tableDefinition.dxfSymbolName) {
table[tableDefinition.tableRecordsProperty] = tableDefinition.parseTableRecords();
} else {
logUnhandledGroup(curr);
curr = scanner.next();
}
break;
default:
logUnhandledGroup(curr);
curr = scanner.next();
}
}
const tableRecords = table[tableDefinition.tableRecordsProperty];
if (tableRecords) {
let actualCount = (() => {
if (tableRecords.constructor === Array) {
return tableRecords.length;
} else if (typeof (tableRecords) === 'object') {
return Object.keys(tableRecords).length;
}
return undefined;
})();
if (expectedCount !== actualCount) log.warn('Parsed ' + actualCount + ' ' + tableDefinition.dxfSymbolName + '\'s but expected ' + expectedCount);
}
curr = scanner.next();
return table;
}
function parseViewPortRecords() {
const viewPorts = [] as IViewPort[]; // 多个表项可以具有相同的名称,表示多个视区配置
let viewPort = {} as IViewPort;
curr = scanner.next();
while (!groupIs(curr, 0, END_OF_TABLE_VALUE)) {
switch (curr.code) {
case 2: // 视口名
viewPort.name = curr.value as string;
curr = scanner.next();
break;
case 10: // 视口的左下角
viewPort.lowerLeftCorner = parsePoint(curr);
curr = scanner.next();
break;
case 11: // 视口右上角
viewPort.upperRightCorner = parsePoint(curr);
curr = scanner.next();
break;
case 12: // 视口中心点(在 DCS 中)
viewPort.center = parsePoint(curr);
curr = scanner.next();
break;
case 13: // 捕捉基点(在 DCS 中)
viewPort.snapBasePoint = parsePoint(curr);
curr = scanner.next();
break;
case 14: // 捕捉间距 X 和 Y
viewPort.snapSpacing = parsePoint(curr);
curr = scanner.next();
break;
case 15: // 栅格间距 X 和 Y
viewPort.gridSpacing = parsePoint(curr);
curr = scanner.next();
break;
case 16: // 相对于目标点的观察方向(在 WCS 中)
viewPort.viewDirectionFromTarget = parsePoint(curr);
curr = scanner.next();
break;
case 17: //观察目标点(在 WCS 中)
viewPort.viewTarget = parsePoint(curr);
curr = scanner.next();
break;
case 42: // 焦距
viewPort.lensLength = curr.value as number;
curr = scanner.next();
break;
case 43: //前向剪裁平面(距目标点的偏移)
viewPort.frontClippingPlane = curr.value;
curr = scanner.next();
break;
case 44: // 后向剪裁平面(距目标点的偏移)
viewPort.backClippingPlane = curr.value;
curr = scanner.next();
break;
case 45: // 视图高度
viewPort.viewHeight = curr.value as number;
curr = scanner.next();
break;
case 50: // 捕捉旋转角度
viewPort.snapRotationAngle = curr.value as number;
curr = scanner.next();
break;
case 51: // 视图扭转角度
viewPort.viewTwistAngle = curr.value as number;
curr = scanner.next();
break;
case 71: // 视图模式(参见 VIEWMODE 系统变量)
viewPort.viewMode = curr.value as string;
curr = scanner.next();
break;
case 79: // UCS 的正交类型 0 = UCS 不正交 1 = 俯视图;2 = 仰视图 3 = 主视图;4 = 后视图 5 = 左视图;6 = 右视图
viewPort.orthographicType = curr.value as string;
curr = scanner.next();
break;
case 110: // UCS 原点(在 DCS 中)
viewPort.ucsOrigin = parsePoint(curr);
curr = scanner.next();
break;
case 111: // UCS X 轴
viewPort.ucsXAxis = parsePoint(curr);
curr = scanner.next();
break;
case 112: // UCS Y 轴
viewPort.ucsYAxis = parsePoint(curr);
curr = scanner.next();
break;
case 281:
/*
* 渲染模式: 0 = 二维优化(传统二维) 1 = 线框图 2 = 隐藏线 3 = 平面着色 4 = 体着色 5 = 带线框平面着色 6 = 带线框体着色
* 所有非二维优化渲染模式均使用新三维图形管道。这些值直接与 SHADEMODE 命令和 AcDbAbstractViewTableRecord::RenderMode 枚举相对应
*/
viewPort.renderMode = curr.value as string;
curr = scanner.next();
break;
case 292:
viewPort.defaultLightingOn = curr.value as string;
curr = scanner.next();
break;
case 330:
viewPort.ownerHandle = curr.value as string;
curr = scanner.next();
break;
case 63: // These are all ambient color. Perhaps should be a gradient when multiple are set.
case 421:
case 431:
viewPort.ambientColor = curr.value as number;
curr = scanner.next();
break;
case 0:
// New ViewPort
if (curr.value === 'VPORT') {
viewPorts.push(viewPort);
viewPort = {} as IViewPort;
curr = scanner.next();
}
break;
default:
logUnhandledGroup(curr);
curr = scanner.next();
break;
}
}
// Note: do not call scanner.next() here,
// parseTable() needs the current group
viewPorts.push(viewPort);
return viewPorts;
}
function parseLineTypes() {
const ltypes = {} as Record<string, ILineType>;
let ltype = {} as ILineType;
let length = 0;
let ltypeName: string;
curr = scanner.next();
while (!groupIs(curr, 0, 'ENDTAB')) {
switch (curr.code) {
case 2:
ltype.name = curr.value as string;
ltypeName = curr.value as string;
curr = scanner.next();
break;
case 3:
ltype.description = curr.value as string;
curr = scanner.next();
break;
case 73: // Number of elements for this line type (dots, dashes, spaces);
length = curr.value as number;
if (length > 0) ltype.pattern = [];
curr = scanner.next();
break;
case 40: // total pattern length
ltype.patternLength = curr.value as number;
curr = scanner.next();
break;
case 49:
ltype.pattern.push(curr.value as string);
curr = scanner.next();
break;
case 0:
if (length > 0 && length !== ltype.pattern.length) log.warn('lengths do not match on LTYPE pattern');
ltypes[ltypeName!] = ltype;
ltype = {} as ILineType;
curr = scanner.next();
break;
default:
curr = scanner.next();
}
}
ltypes[ltypeName!] = ltype;
return ltypes;
}
function parseLayers() {
const layers = {} as Record<string, ILayer>;
let layer = {} as ILayer;
let layerName: string | undefined;
curr = scanner.next();
while (!groupIs(curr, 0, 'ENDTAB')) {
switch (curr.code) {
case 2: // layer name
layer.name = curr.value as string;
layerName = curr.value as string;
curr = scanner.next();
break;
case 62: // color, visibility
// @ts-ignore
layer.visible = curr.value >= 0;
// TODO 0 and 256 are BYBLOCK and BYLAYER respectively. Need to handle these values for layers?.
layer.colorIndex = Math.abs(curr.value as number);
layer.color = getAcadColor(layer.colorIndex as number);
curr = scanner.next();
break;
case 70: // frozen layer
layer.frozen = (((curr.value as number) & 1) != 0 || ((curr.value as number) & 2) != 0);
curr = scanner.next();
break;
case 0:
// New Layer
if (curr.value === 'LAYER') {
layers[layerName!] = layer;
layer = {} as ILayer;
layerName = undefined;
curr = scanner.next();
}
break;
default:
logUnhandledGroup(curr);
curr = scanner.next();
break;
}
}
// Note: do not call scanner.next() here,
// parseLayerTable() needs the current group
layers[layerName!] = layer;
return layers;
}
function parseDimStyles() {
let dimStyles = {},
styleName: string | undefined,
style: any = {};
curr = scanner.next();
while (!groupIs(curr, 0, 'ENDTAB')) {
if (dimStyleCodes.has(curr.code)) {
style[dimStyleCodes.get(curr.code) as string] = curr.value
curr = scanner.next();
} else {
switch (curr.code) {
case 2: // style name
style.name = curr.value;
styleName = curr.value as string;
curr = scanner.next();
break;
case 0:
// New style
if (curr.value === 'DIMSTYLE') {
// @ts-ignore
dimStyles[styleName] = style;
style = {};
styleName = undefined;
curr = scanner.next();
}
break;
default:
logUnhandledGroup(curr);
curr = scanner.next();
break;
}
}
}
// Note: do not call scanner.next() here,
// parseLayerTable() needs the current group
dimStyles[styleName as string] = style;
return dimStyles;
}
const parseStyles = function () {
const styles = {};
let style: any = {};
let styleName;
curr = scanner.next();
while (!groupIs(curr, 0, END_OF_TABLE_VALUE)) {
switch (curr.code) {
case 100:
style.subClassMarker = curr.value;
curr = scanner.next();
break;
case 2:
style.styleName = curr.value;
styleName = curr.value;
curr = scanner.next();
break;
case 70:
style.standardFlag = curr.value;
curr = scanner.next();
break;
case 40:
style.fixedTextHeight = curr.value;
curr = scanner.next();
break;
case 41:
style.widthFactor = curr.value;
curr = scanner.next();
break;
case 50:
style.obliqueAngle = curr.value;
curr = scanner.next();
break;
case 71:
style.textGenerationFlag = curr.value;
curr = scanner.next();
break;
case 42:
style.lastHeight = curr.value;
curr = scanner.next();
break;
case 3:
style.font = curr.value;
curr = scanner.next();
break;
case 4:
style.bigFont = curr.value;
curr = scanner.next();
break;
case 1071:
style.extendedFont = curr.value;
curr = scanner.next();
break;
case 0:
if (curr.value === 'STYLE') {
styles[styleName] = style;
style = {};
styleName = undefined;
curr = scanner.next();
}
break;
default:
logUnhandledGroup(curr);
curr = scanner.next();
break;
}
}
styles[styleName] = style;
return styles;
};
const tableDefinitions = {
VPORT: {
tableRecordsProperty: 'viewPorts',
tableName: 'viewPort',
dxfSymbolName: 'VPORT',
parseTableRecords: parseViewPortRecords
},
LTYPE: {
tableRecordsProperty: 'lineTypes',
tableName: 'lineType',
dxfSymbolName: 'LTYPE',
parseTableRecords: parseLineTypes
},
LAYER: {
tableRecordsProperty: 'layers',
tableName: 'layer',
dxfSymbolName: 'LAYER',
parseTableRecords: parseLayers
},
DIMSTYLE: {
tableRecordsProperty: 'dimStyles',
tableName: 'dimstyle',
dxfSymbolName: 'DIMSTYLE',
parseTableRecords: parseDimStyles
},
STYLE: {
tableRecordsProperty: 'styles',
tableName: 'style',
dxfSymbolName: 'STYLE',
parseTableRecords: parseStyles,
},
} as ITableDefinitions;
/**
* 在解析器首次读取0:ENTITIES组后调用。扫描器应该已经在第一个实体的开始。
* @return {Array} the resulting entities
*/
function parseEntities(forBlock: boolean) {
const entities = [] as IEntity[];
const endingOnValue = forBlock ? 'ENDBLK' : 'ENDSEC';
if (!forBlock) {
curr = scanner.next();
}
while (true) {
if (curr.code === 0) {
if (curr.value === endingOnValue) {
break;
}
const handler = self._entityHandlers[curr.value as EntityName];
if (handler != null) {
const entity = handler.parseEntity(scanner, curr);
curr = scanner.lastReadGroup!;
ensureHandle(entity);
entities.push(entity);
} else {
// log.debug('Unhandled entity ' + curr.value);
curr = scanner.next();
continue;
}
} else {
// ignored lines from unsupported entity
curr = scanner.next();
}
}
if (endingOnValue == 'ENDSEC') curr = scanner.next(); // swallow up ENDSEC, but not ENDBLK
return entities;
}
/**
* 解析一个2D或3D点.
* 如果它是3D的,将其作为具有x, y和(有时)z属性的对象返回。
* 假定当前组是被读入点的x,并且scanner.next()将返回y。
* 解析器将自动确定是否存在z点。
* @return {Object} 二维或三维点作为具有 x、y[,z] 的对象
*/
function parsePoint(curr: IGroup) {
const point = {} as IPoint;
let code = curr.code;
point.x = curr.value as number;
code += 10;
curr = scanner.next();
if (curr.code != code)
throw new Error('Expected code for point value to be ' + code +
' but got ' + curr.code + '.');
point.y = curr.value as number;
code += 10;
curr = scanner.next();
if (curr.code != code) {
scanner.rewind();
return point;
}
point.z = curr.value as number;
return point;
}
function ensureHandle(entity: IEntity | IBlock) {
if (!entity) throw new TypeError('entity cannot be undefined or null');
if (!entity.handle) entity.handle = lastHandle++;
}
parseAll();
return dxf;
}
}
function groupIs(group: IGroup, code: number, value: string | number | boolean) {
return group.code === code && group.value === value;
}
// @ts-ignore
function logUnhandledGroup(curr: IGroup) {
// log.debug('unhandled group ' + debugCode(curr));
}
function debugCode(curr: IGroup) {
return curr.code + ':' + curr.value;
}
/**
* Returns the truecolor value of the given AutoCad color index value
* @return {Number} truecolor value as a number
*/
function getAcadColor(index: number) {
return AUTO_CAD_COLOR_INDEX[index];
}
// const BLOCK_ANONYMOUS_FLAG = 1;
// const BLOCK_NON_CONSTANT_FLAG = 2;
// const BLOCK_XREF_FLAG = 4;
// const BLOCK_XREF_OVERLAY_FLAG = 8;
// const BLOCK_EXTERNALLY_DEPENDENT_FLAG = 16;
// const BLOCK_RESOLVED_OR_DEPENDENT_FLAG = 32;
// const BLOCK_REFERENCED_XREF = 64;
/* Notes */
// Code 6 of an entity indicates inheritance of properties (eg. color).
// BYBLOCK means inherits from block
// BYLAYER (default) mean inherits from layer
@@ -0,0 +1,121 @@
import log from '@/utils/log/Logger';
export default class ExtendedDataParser {
private appName: null;
private appNameWarningShown: boolean;
private lastString: any;
private sectionStack: { values: any[] }[];
private failure: boolean;
constructor() {
this.appName = null
this.appNameWarningShown = false
this.lastString = null
this.sectionStack = [this._CreateSection()]
this.failure = false
}
/**
* Feed next token.
* @return {boolean} 如果应该为此令牌创建新的解析器实例,则为True。
*/
Feed(curr) {
if (!this.appName) {
if (curr.code == 1001) {
this.appName = curr.value
return false
}
if (!this.appNameWarningShown) {
this.appNameWarningShown = true
log.warn("XDATA部分不以应用程序名称开头")
}
return false
}
if (curr.code == 1001) {
return true
}
if (this.failure) {
/* Ignore all the rest content in case of parsing failure. */
return false
}
if (curr.code == 1000) {
if (this.lastString) {
log.warn("XDATA section unused string: " + this.lastString)
}
this.lastString = curr.value
return false
}
const curSection = this._currentSection
if (curr.code == 1002) {
if (curr.value == "{") {
if (!this.lastString) {
log.warn("Unnamed XDATA section encountered")
this.failure = true
return false
}
const newSection = this._CreateSection()
curSection[this.lastString] = newSection
this.lastString = null
this.sectionStack.push(newSection)
return false
}
if (curr.value == "}") {
if (this.sectionStack.length < 2) {
log.warn("Unmatched XDATA section closing")
this.failure = true
return false
}
this.sectionStack.length = this.sectionStack.length - 1
return false
}
log.warn("Bad XDATA section control string encountered: " + curr.value)
this.failure = true
return false
}
if (this.lastString !== null) {
curSection.values.push(this._CreateValue(1000, this.lastString))
this.lastString = null
}
curSection.values.push(this._CreateValue(curr.code, curr.value))
return false
}
/** Finalize XDATA section parsing. */
Finish(entity) {
if (this.failure) {
return
}
if (this.appName) {
let xdata
if (entity.hasOwnProperty("xdata")) {
xdata = entity.xdata
} else {
xdata = {}
entity.xdata = xdata
}
xdata[this.appName] = this.sectionStack[0]
}
}
get _currentSection() {
return this.sectionStack[this.sectionStack.length - 1]
}
_CreateSection() {
return {
values: []
}
}
_CreateValue(code, value) {
return {code, value}
}
}
+156
View File
@@ -0,0 +1,156 @@
import AUTO_CAD_COLOR_INDEX from './AutoCadColorIndex';
import ExtendedDataParser from './ExtendedDataParser';
import DxfArrayScanner, { IGroup } from './DxfArrayScanner';
import { IEntity, IPoint } from './entities/geomtry';
/**
* 返回给定AutoCad颜色索引值的对应color值
* @return {Number} color value as a number
*/
export function getAcadColor(index: number) {
return AUTO_CAD_COLOR_INDEX[index];
}
/**
* 解析2D或3D坐标、矢量或点。完成后,扫描器保持在坐标的最后一组上。
* @param {*} scanner
*/
export function parsePoint(scanner: DxfArrayScanner) {
const point = {} as IPoint;
// 重读第一个坐标的组
scanner.rewind();
let curr = scanner.next();
let code = curr.code;
point.x = curr.value as number;
code += 10;
curr = scanner.next();
if (curr.code != code)
throw new Error('Expected code for point value to be ' + code +
' but got ' + curr.code + '.');
point.y = curr.value as number;
code += 10;
curr = scanner.next();
if (curr.code != code) {
// Only the x and y are specified. Don't read z.
scanner.rewind(); // Let the calling code advance off the point
return point;
}
point.z = curr.value as number;
return point;
}
/**
* 某些实体可能包含由组101开始的嵌入对象。实体结束前的所有其他数据不应被解释为实体属性。该特性没有相关文档。
* @param scanner
*/
export function skipEmbeddedObject(scanner: DxfArrayScanner) {
/* 确保正确启动组. */
scanner.rewind()
let curr = scanner.next()
if (curr.code !== 101) {
throw new Error("Bad call for skipEmbeddedObject()")
}
do {
curr = scanner.next()
} while (curr.code !== 0)
scanner.rewind()
}
/**
* 尝试解析所有实体通用的代码。如果组由此函数处理,则返回true。
* @param {*} entity - the entity currently being parsed
* @param {*} curr - the current group being parsed
* @param scanner
*/
export function checkCommonEntityProperties(entity: IEntity, curr:IGroup, scanner:DxfArrayScanner) {
let xdataParser;
while (curr.code >= 1000) {
if (!xdataParser) {
xdataParser = new ExtendedDataParser()
}
if (xdataParser.Feed(curr)) {
xdataParser.Finish(entity)
xdataParser = null;
} else {
curr = scanner.next()
}
}
if (xdataParser) {
xdataParser.Finish(entity);
scanner.rewind();
return true;
}
switch (curr.code) {
case 0:
entity.type = curr.value as string;
break;
case 5:
entity.handle = curr.value as number;
break;
case 6:
entity.lineType = curr.value as string;
break;
case 8: // Layer name
entity.layer = curr.value as string;
break;
case 48:
entity.lineTypeScale = curr.value as number;
break;
case 60:
entity.visible = curr.value === 0;
break;
case 62: // Acad Index Color. 0 inherits ByBlock. 256 inherits ByLayer. Default is bylayer
entity.colorIndex = curr.value as number;
entity.color = getAcadColor(Math.abs(curr.value as number));
break;
case 67:
// 不存在或0表示图元位于模型空间中。
// 1 表示图元位于图纸空间中(可选)
entity.inPaperSpace = curr.value !== 0;
break;
case 100:
//ignore
break;
case 101: // Embedded Object in ACAD 2018.
// See https://ezdxf.readthedocs.io/en/master/dxfinternals/dxftags.html#embedded-objects
while (curr.code != 0) {
curr = scanner.next();
}
scanner.rewind();
break;
case 330:
entity.ownerHandle = curr.value as string;
break;
case 347:
entity.materialObjectHandle = curr.value as number;
break;
case 370:
//From https://www.woutware.com/Forum/Topic/955/lineweight?returnUrl=%2FForum%2FUserPosts%3FuserId%3D478262319
// An integer representing 100th of mm, must be one of the following values:
// 0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211.
// -3 = STANDARD, -2 = BYLAYER, -1 = BYBLOCK
entity.lineweight = curr.value as 0| 5| 9| 13| 15| 18| 20| 25| 30| 35| 40| 50| 53| 60| 70| 80| 90| 100| 106| 120| 140| 158| 200| 211|-3|-2|-1;
break;
case 420: // TrueColor Color
entity.color = curr.value as number;
break;
case 1000:
entity.extendedData = entity.extendedData || {};
entity.extendedData.customStrings = entity.extendedData.customStrings || [];
entity.extendedData.customStrings.push(curr.value as string);
break;
case 1001:
entity.extendedData = entity.extendedData || {};
entity.extendedData.applicationName = curr.value as string;
break;
default:
return false;
}
return true;
}
@@ -0,0 +1,88 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface I3DfaceEntity extends IEntity {
shape: boolean;
hasContinuousLinetypePattern: boolean;
vertices: IPoint[];
}
export default class ThreeDface implements IGeometry {
public ForEntityName = '3DFACE' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value as string, vertices: [] as IPoint[] } as I3DfaceEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 70: // 1 = Closed shape, 128 = plinegen?, 0 = default
entity.shape = (((curr.value as number) & 1) === 1);
entity.hasContinuousLinetypePattern = (((curr.value as number) & 128) === 128);
break;
case 10: // X coordinate of point
entity.vertices = parse3dFaceVertices(scanner, curr);
curr = scanner.lastReadGroup as IGroup;
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
function parse3dFaceVertices(scanner:DxfArrayScanner, curr:IGroup) {
var vertices:IPoint[] = [];
var vertexIsStarted = false;
var vertexIsFinished = false;
var verticesPer3dFace = 4; // there can be up to four vertices per face, although 3 is most used for TIN
for (let i = 0; i <= verticesPer3dFace; i++) {
var vertex = {} as IPoint;
while (!scanner.isEOF()) {
if (curr.code === 0 || vertexIsFinished) break;
switch (curr.code) {
case 10: // X0
case 11: // X1
case 12: // X2
case 13: // X3
if (vertexIsStarted) {
vertexIsFinished = true;
continue;
}
vertex.x = curr.value as number;
vertexIsStarted = true;
break;
case 20: // Y
case 21:
case 22:
case 23:
vertex.y = curr.value as number;
break;
case 30: // Z
case 31:
case 32:
case 33:
vertex.z = curr.value as number;
break;
default:
// it is possible to have entity codes after the vertices.
// So if code is not accounted for return to entity parser where it might be accounted for
return vertices;
}
curr = scanner.next();
}
// See https://groups.google.com/forum/#!topic/comp.cad.autocad/9gn8s5O_w6E
vertices.push(vertex);
vertexIsStarted = false;
vertexIsFinished = false;
}
scanner.rewind();
return vertices;
};
@@ -0,0 +1,57 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IArcEntity extends IEntity {
center: IPoint;
radius: number;
startAngle: number;
endAngle: number;
angleLength: number;
extrusionDirectionX: number;
extrusionDirectionY: number;
extrusionDirectionZ: number;
}
export default class Arc implements IGeometry {
public ForEntityName = 'ARC' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value } as IArcEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 10: // X coordinate of point
entity.center = helpers.parsePoint(scanner);
break;
case 40: // radius
entity.radius = curr.value as number;
break;
case 50: // start angle
entity.startAngle = Math.PI / 180 * (curr.value as number);
break;
case 51: // end angle
entity.endAngle = Math.PI / 180 * (curr.value as number);
entity.angleLength = entity.endAngle - entity.startAngle; // angleLength is deprecated
break;
case 210:
entity.extrusionDirectionX = curr.value as number;
break;
case 220:
entity.extrusionDirectionY = curr.value as number;
break;
case 230:
entity.extrusionDirectionZ = curr.value as number;
break;
default: // ignored attribute
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,119 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IAttdefEntity extends IEntity {
scale: number;
textStyle: 'STANDARD' | string;
text: string;
tag: string;
prompt: string;
startPoint: IPoint;
endPoint: IPoint;
thickness: number;
textHeight: number;
rotation: number;
obliqueAngle: number;
invisible: boolean;
constant: boolean;
verificationRequired: boolean;
preset: boolean;
backwards: boolean;
mirrored: boolean;
horizontalJustification: number;
fieldLength: number;
verticalJustification: number;
extrusionDirectionX: number;
extrusionDirectionY: number;
extrusionDirectionZ: number;
}
export default class Attdef implements IGeometry {
public ForEntityName = 'ATTDEF' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = {
type: curr.value,
scale: 1,
textStyle: 'STANDARD'
} as IAttdefEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) {
break;
}
switch (curr.code) {
case 1:
entity.text = curr.value as string;
break;
case 2:
entity.tag = curr.value as string;
break;
case 3:
entity.prompt = curr.value as string;
break;
case 7:
entity.textStyle = curr.value as string;
break;
case 10: // X coordinate of 'first alignment point'
entity.startPoint = helpers.parsePoint(scanner);
break;
case 11: // X coordinate of 'second alignment point'
entity.endPoint = helpers.parsePoint(scanner);
break;
case 39:
entity.thickness = curr.value as number;
break;
case 40:
entity.textHeight = curr.value as number;
break;
case 41:
entity.scale = curr.value as number;
break;
case 50:
entity.rotation = curr.value as number;
break;
case 51:
entity.obliqueAngle = curr.value as number;
break;
case 70:
entity.invisible = !!((curr.value as number) & 0x01);
entity.constant = !!((curr.value as number) & 0x02);
entity.verificationRequired = !!((curr.value as number) & 0x04);
entity.preset = !!((curr.value as number) & 0x08);
break;
case 71:
entity.backwards = !!((curr.value as number) & 0x02);
entity.mirrored = !!((curr.value as number) & 0x04);
break;
case 72:
// TODO: enum values?
entity.horizontalJustification = curr.value as number;
break;
case 73:
entity.fieldLength = curr.value as number;
break;
case 74:
// TODO: enum values?
entity.verticalJustification = curr.value as number;
break;
case 100:
break;
case 210:
entity.extrusionDirectionX = curr.value as number;
break;
case 220:
entity.extrusionDirectionY = curr.value as number;
break;
case 230:
entity.extrusionDirectionZ = curr.value as number;
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,166 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers';
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IAttribEntity extends IEntity {
scale: number;
textStyle: 'STANDARD' | string;
text: string;
tag: string;
prompt: string;
startPoint: IPoint;
endPoint: IPoint;
thickness: number;
textHeight: number;
rotation: number;
lineSpacingFactor: number;
fillBoxScale: number;
annotationHeight: number;
obliqueAngle: number;
invisible: boolean;
constant: boolean;
verificationRequired: boolean;
preset: boolean;
horizontalJustification: number;
verticalJustification: number;
backgroundFillColor: string;
attachmentPoint: number;
lineSpacing: number;
backgroundFillSetting: string;
extrusionDirection: IPoint;
lockPositionFlag: number;
hardPointerId: number;
}
export default class Attrib implements IGeometry{
public ForEntityName = 'ATTRIB' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = {
type: curr.value,
scale: 1,
textStyle: 'STANDARD',
} as IAttribEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
// 0 为图元类型 (MTEXT),不写入,此处当作 Attrib解析
if (curr.code === 0) {
break;
}
switch (curr.code) {
case 1:
// 字符串本身
entity.text = curr.value as string;
break;
case 2:
// 属性标签(字符串;不能包含空格)
entity.tag = curr.value as string;
break;
case 3:
// 附加文字(始终在长度为 250 个字符的数据块中)(可选)
entity.prompt = curr.value as string;
break;
case 7:
// DXF:X 值;APP:三维矢量文字样式名(如果未提供,则为 STANDARD)(可选)
entity.textStyle = curr.value as string;
break;
case 10: // 文字起点(在 OCS 中)
entity.startPoint = helpers.parsePoint(scanner);
break;
case 11: //X 轴方向矢量(在 WCS 中)
entity.endPoint = helpers.parsePoint(scanner);
break;
case 39:
// 厚度(可选;默认值 = 0
entity.thickness = curr.value as number;
break;
case 40:
// 文字高度
entity.textHeight = curr.value as number;
break;
case 41:
// X 相对缩放比例(宽度)(可选;默认值 = 1)。使用拟合类型的文字时,该值也将进行调整。
entity.scale = curr.value as number;
break;
case 44:
// 多行文字的行距比例(可选):
// 要应用的行距的默认百分比(五分之三)。
// 有效值的范围从 0.25 到 4.00
entity.lineSpacingFactor = curr.value as number;
break;
case 45:
// 填充框大小(可选):
// 确定文字周围的边框大小。
entity.fillBoxScale = curr.value as number;
break;
case 46:
// 定义注释高度
entity.annotationHeight = curr.value as number;
break;
case 50:
//文字旋转角度(可选;默认值 = 0)
entity.rotation = curr.value as number;
break;
case 51:
// 倾斜角(可选;默认值 = 0
entity.obliqueAngle = curr.value as number;
break;
case 63:
//背景填充颜色(可选):
// 组码 90 为 1 时要用于背景填充的颜色。
entity.backgroundFillColor = curr.value as string
break;
case 70:
entity.invisible = !!((curr.value as number) & 0x01);
entity.constant = !!((curr.value as number) & 0x02);
entity.verificationRequired = !!((curr.value as number) & 0x04);
entity.preset = !!((curr.value as number) & 0x08);
break;
case 71:
//附着点:
// 1 = 左上;2 = 中上;3 = 右上
// 4 = 左中;5 = 正中;6 = 右中
// 7 = 左下;8 = 中下;9 = 右下
entity.attachmentPoint = curr.value as number;
break;
case 72:
// 文字水平对正类型(可选;默认值 = 0)
entity.horizontalJustification = curr.value as number;
break;
case 73:
// 字段长度(可选;默认值 = 0)(当前未使用)
entity.lineSpacing = curr.value as number;
break;
case 74:
// 垂直文字对正类型(可选;默认值 = 0)
entity.verticalJustification = curr.value as number;
break;
case 90:
// 背景填充设置:
// 0 = 背景填充关闭
// 1 = 使用背景填充颜色
// 2 = 使用图形窗口颜色作为背景填充颜色
entity.backgroundFillSetting = curr.value as string;
break;
case 210:
// 拉伸方向。仅当图元的拉伸方向与 WCS 的 Z 轴不平行时才出现(可选;默认值 = 0, 0, 1)
entity.extrusionDirection = helpers.parsePoint(scanner);
break;
case 280:
// 锁定位置标志。锁定块参照中属性的位置
entity.lockPositionFlag = curr.value as number;
break;
case 340:
// 次要属性或属性定义的硬指针 ID
entity.hardPointerId = curr.value as number;
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,48 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface ICircleEntity extends IEntity {
center: IPoint;
radius: number;
startAngle: number;
endAngle: number;
angleLength: number;
}
export default class Circle implements IGeometry {
public ForEntityName = 'CIRCLE' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value } as ICircleEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 10: // X coordinate of point
entity.center = helpers.parsePoint(scanner);
break;
case 40: // radius
entity.radius = (curr.value as number);
break;
case 50: // start angle
entity.startAngle = Math.PI / 180 * (curr.value as number);
break;
case 51: // end angle
const endAngle = Math.PI / 180 * (curr.value as number);
if (endAngle < entity.startAngle)
entity.angleLength = endAngle + 2 * Math.PI - entity.startAngle;
else
entity.angleLength = endAngle - entity.startAngle;
entity.endAngle = endAngle;
break;
default: // ignored attribute
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,79 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IDimensionEntity extends IEntity{
block: string;
anchorPoint: IPoint;
middleOfText: IPoint;
insertionPoint: IPoint;
linearOrAngularPoint1: IPoint;
linearOrAngularPoint2: IPoint;
diameterOrRadiusPoint: IPoint;
arcPoint: IPoint;
dimensionType: number;
attachmentPoint: number;
actualMeasurement: number;
text: string;
angle: number;
}
export default class Dimension implements IGeometry {
public ForEntityName = 'DIMENSION' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value } as IDimensionEntity;
curr = scanner.next();
while(!scanner.isEOF()) {
if(curr.code === 0) break;
switch(curr.code) {
case 2: // Referenced block name
entity.block = curr.value as string;
break;
case 10: // X coordinate of 'first alignment point'
entity.anchorPoint = helpers.parsePoint(scanner);
break;
case 11:
entity.middleOfText = helpers.parsePoint(scanner);
break;
case 12: // Insertion point for clones of a dimension
entity.insertionPoint = helpers.parsePoint(scanner);
break;
case 13: // Definition point for linear and angular dimensions
entity.linearOrAngularPoint1 = helpers.parsePoint(scanner);
break;
case 14: // Definition point for linear and angular dimensions
entity.linearOrAngularPoint2 = helpers.parsePoint(scanner);
break;
case 15: // Definition point for diameter, radius, and angular dimensions
entity.diameterOrRadiusPoint = helpers.parsePoint(scanner);
break;
case 16: // Point defining dimension arc for angular dimensions
entity.arcPoint = helpers.parsePoint(scanner);
break;
case 70: // Dimension type
entity.dimensionType = curr.value as number;
break;
case 71: // 5 = Middle center
entity.attachmentPoint = curr.value as number;
break;
case 42: // Actual measurement
entity.actualMeasurement = curr.value as number;
break;
case 1: // Text entered by user explicitly
entity.text = curr.value as string;
break;
case 50: // Angle of rotated, horizontal, or vertical dimensions
entity.angle = curr.value as number;
break;
default: // check common entity attributes
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,52 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IEllipseEntity extends IEntity {
center: IPoint;
majorAxisEndPoint: IPoint;
axisRatio: number;
startAngle: number;
endAngle: number;
name: string;
}
export default class Ellipse implements IGeometry {
public ForEntityName = 'ELLIPSE' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value } as IEllipseEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 10:
entity.center = helpers.parsePoint(scanner);
break;
case 11:
entity.majorAxisEndPoint = helpers.parsePoint(scanner);
break;
case 40:
entity.axisRatio = curr.value as number;
break;
case 41:
entity.startAngle = curr.value as number;
break;
case 42:
entity.endAngle = curr.value as number;
break;
case 2:
entity.name = curr.value as string;
break;
default: // check common entity attributes
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,50 @@
import DxfArrayScanner, { IGroup } from "../DxfArrayScanner";
export interface IPoint {
x: number;
y: number;
z: number;
}
export interface IEntity {
lineType: string;
layer: string;
lineTypeScale: number;
visible: boolean;
colorIndex: number;
color: number;
inPaperSpace: boolean;
ownerHandle: string;
materialObjectHandle: number;
lineweight: 0| 5| 9| 13| 15| 18| 20| 25| 30| 35| 40| 50| 53| 60| 70| 80| 90| 100| 106| 120| 140| 158| 200| 211|-3|-2|-1;
extendedData: {
customStrings: string[];
applicationName: string;
};
type: string;
handle: number;
}
export type EntityName = 'POINT'
| '3DFACE'
| 'ARC'
| 'ATTDEF'
| 'ATTRIB'
| 'CIRCLE'
| 'DIMENSION'
| 'ELLIPSE'
| 'HATCH'
| 'INSERT'
| 'LINE'
| 'LWPOLYLINE'
| 'MTEXT'
| 'POLYLINE'
| 'SOLID'
| 'SPLINE'
| 'TEXT'
| 'VERTEX';
export default interface IGeometry {
ForEntityName: EntityName;
parseEntity(scanner: DxfArrayScanner, curr: IGroup): IEntity;
}
@@ -0,0 +1,346 @@
import DxfArrayScanner, {IGroup} from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, {IEntity, IPoint} from './geomtry';
export interface IHatchEntity extends IEntity {
boundaryLoops: any[];
definitionLines: any[];
seedPoints: any[];
patternName: string;
isSolid: boolean;
hatchStyle: string;
patternType: number;
patternAngle: number;
patternScale: number;
}
export default class Hatch implements IGeometry {
public ForEntityName = 'HATCH' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
let entity = {type: curr.value} as IHatchEntity;
let numBoundaryLoops = 0;
let numDefinitionLines = 0;
let numSeedPoints = 0;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
while (numBoundaryLoops > 0) {
const loop = ParseBoundaryLoop(curr, scanner)
if (loop) {
entity.boundaryLoops.push(loop);
numBoundaryLoops--;
curr = scanner.next();
} else {
numBoundaryLoops = 0
}
}
while (numDefinitionLines > 0) {
const line = ParseDefinitionLine(curr, scanner)
if (line) {
entity.definitionLines.push(line);
numDefinitionLines--;
curr = scanner.next();
} else {
numDefinitionLines = 0
}
}
while (numSeedPoints > 0) {
const pt = ParseSeedPoint(curr, scanner);
if (pt) {
entity.seedPoints.push(pt);
numSeedPoints--;
curr = scanner.next();
} else {
numSeedPoints = 0
}
}
if (curr.code === 0) break;
switch (curr.code) {
case 2: // 填充图案名
entity.patternName = curr.value as string;
break;
case 70: // 实体填充标志(实体填充 = 1;图案填充 = 0)
entity.isSolid = curr.value != 0;
break;
case 91: // 边界路径(环)数
numBoundaryLoops = curr.value as number;
if (numBoundaryLoops > 0) {
entity.boundaryLoops = []
}
break;
/**
* 图案填充样式:
* 0 = 对“奇数奇偶校验”区域进行图案填充(普通样式)
* 1 = 仅对最外层区域进行图案填充(“外部”样式)
* 2 = 对整个区域进行图案填充(“忽略”样式)
*/
case 75:
entity.hatchStyle = curr.value as string;
break;
/**
* 填充图案类型:
* 0 = 用户定义;1 = 预定义;2 = 自定义
*/
case 76:
entity.patternType = curr.value as number;
break;
case 52: // 填充图案角度(仅限图案填充)
entity.patternAngle = (curr.value as number) * Math.PI / 180;
break;
case 41: // 填充图案比例或间距(仅限图案填充)
entity.patternScale = curr.value as number;
break;
case 78: // 图案定义直线数
numDefinitionLines = curr.value as number;
if (numDefinitionLines > 0) {
entity.definitionLines = []
}
break;
case 98: // 种子点数
numSeedPoints = curr.value as number;
if (numSeedPoints > 0) {
entity.seedPoints = []
}
break;
default: // 检查通用实体属性
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
};
}
function ParseBoundaryLoop(curr: IGroup, scanner: DxfArrayScanner) {
let entity:any = null
const ParsePolyline = () => {
const pl: { vertices: IPoint[], isClosed: boolean } = {vertices: [], isClosed: false};
let numVertices = 0;
while (true) {
if (numVertices > 0) {
for (let i = 0; i < numVertices; i++) {
if (curr.code != 10) {
break
}
const p = helpers.parsePoint(scanner)
curr = scanner.next();
if (curr.code == 42) {
// @ts-ignore
p.bulge = curr.value;
curr = scanner.next();
}
pl.vertices.push(p)
}
return pl
}
switch (curr.code) {
case 73:
pl.isClosed = curr.value as boolean;
break;
case 93:
numVertices = curr.value as number;
break;
default:
return pl;
}
curr = scanner.next();
}
}
const ParseEdge = () => {
if (curr.code != 72) {
return null
}
const e:any = {type: curr.value}
curr = scanner.next();
const isSpline = e.type == 4;
while (true) {
switch (curr.code) {
case 10:
if (isSpline) {
if (!e.controlPoints) {
e.controlPoints = [];
}
e.controlPoints.push(helpers.parsePoint(scanner));
} else {
e.start = helpers.parsePoint(scanner);
}
break;
case 11:
if (isSpline) {
if (!e.fitPoints) {
e.fitPoints = [];
}
e.fitPoints.push(helpers.parsePoint(scanner));
} else {
e.end = helpers.parsePoint(scanner);
}
break;
case 40:
if (isSpline) {
if (!e.knotValues) {
e.knotValues = [];
}
e.knotValues.push(curr.value);
} else {
e.radius = curr.value;
}
break;
case 50:
e.startAngle = (curr.value as number) * Math.PI / 180;
break;
case 51:
e.endAngle = (curr.value as number) * Math.PI / 180;
break;
case 73:
if (isSpline) {
e.rational = curr.value;
} else {
e.isCcw = curr.value;
}
break;
case 74:
e.periodic = curr.value;
break;
case 94:
e.degreeOfSplineCurve = curr.value;
break;
//XXX暂时忽略一些群体,主要是样条
case 95:
case 96:
case 42:
case 97:
break;
default:
return e
}
curr = scanner.next();
}
}
let polylineParsed = false;
let numEdges = 0;
let numSourceRefs = 0;
while (true) {
if (!entity) {
if (curr.code != 92) {
return null;
}
entity = {type: curr.value};
curr = scanner.next();
}
if ((entity.type & 2) && !polylineParsed) {
entity.polyline = ParsePolyline()
polylineParsed = true
}
while (numEdges) {
const edge = ParseEdge();
if (edge) {
entity.edges.push(edge);
numEdges--;
} else {
numEdges = 0;
}
}
while (numSourceRefs) {
if (curr.code == 330) {
entity.sourceRefs.push(curr.value);
numSourceRefs--;
curr = scanner.next();
} else {
numSourceRefs = 0
}
}
switch (curr.code) {
case 93:
numEdges = curr.value as number;
if (numEdges > 0) {
entity.edges = []
}
break;
case 97:
numSourceRefs = curr.value as number;
if (numSourceRefs > 0) {
entity.sourceRefs = []
}
break;
default:
scanner.rewind();
return entity;
}
curr = scanner.next();
}
}
function ParseDefinitionLine(curr: IGroup, scanner: DxfArrayScanner) {
/* 假设总是从53组开始. */
if (curr.code != 53) {
return null
}
const entity:any = {
angle: (curr.value as number) * Math.PI / 180,
base: {x: 0, y: 0},
offset: {x: 0, y: 0}
};
curr = scanner.next();
let numDashes = 0;
while (true) {
switch (curr.code) {
case 43:
entity.base.x = curr.value as number;
break;
case 44:
entity.base.y = curr.value as number;
break;
case 45:
entity.offset.x = curr.value as number;
break;
case 46:
entity.offset.y = curr.value as number;
break;
case 49:
if (numDashes > 0) {
entity.dashes.push(curr.value);
numDashes--;
}
break;
case 79:
numDashes = curr.value as number;
if (curr.value) {
entity.dashes = []
}
break;
default:
scanner.rewind();
return entity;
}
curr = scanner.next();
}
}
function ParseSeedPoint(curr: IGroup, scanner: DxfArrayScanner) {
if (curr.code != 10) {
return null
}
return helpers.parsePoint(scanner);
}
@@ -0,0 +1,70 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IInsertEntity extends IEntity {
name: string;
xScale: number;
yScale: number;
zScale: number;
position: IPoint;
rotation: number;
columnCount: number;
rowCount: number;
columnSpacing: number;
rowSpacing: number;
extrusionDirection: IPoint;
}
export default class Insert implements IGeometry {
public ForEntityName = 'INSERT' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value } as IInsertEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 2:
entity.name = curr.value as string;
break;
case 41:
entity.xScale = curr.value as number;
break;
case 42:
entity.yScale = curr.value as number;
break;
case 43:
entity.zScale = curr.value as number;
break;
case 10:
entity.position = helpers.parsePoint(scanner);
break;
case 50:
entity.rotation = curr.value as number;
break;
case 70:
entity.columnCount = curr.value as number;
break;
case 71:
entity.rowCount = curr.value as number;
break;
case 44:
entity.columnSpacing = curr.value as number;
break;
case 45:
entity.rowSpacing = curr.value as number;
break;
case 210:
entity.extrusionDirection = helpers.parsePoint(scanner);
break;
default: // check common entity attributes
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,40 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface ILineEntity extends IEntity{
vertices: IPoint[];
extrusionDirection: IPoint;
}
export default class Line implements IGeometry{
public ForEntityName= 'LINE' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value, vertices: [] as IPoint[] } as ILineEntity;
curr = scanner.next();
while(!scanner.isEOF()) {
if(curr.code === 0) break;
switch(curr.code) {
case 10: // X coordinate of point
entity.vertices.unshift(helpers.parsePoint(scanner));
break;
case 11:
entity.vertices.push(helpers.parsePoint(scanner));
break;
case 210:
entity.extrusionDirection = helpers.parsePoint(scanner);
break;
case 100:
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,125 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IVertex extends IPoint{
startWidth: number;
endWidth: number;
bulge: number;
}
export interface ILwpolylineEntity extends IEntity {
vertices: IVertex[];
elevation: number;
depth: number;
shape: boolean;
hasContinuousLinetypePattern: boolean;
width: number;
extrusionDirectionX: number;
extrusionDirectionY: number;
extrusionDirectionZ: number;
}
export default class Lwpolyline implements IGeometry {
public ForEntityName = 'LWPOLYLINE' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value, vertices: [] as IVertex[] } as ILwpolylineEntity;
let numberOfVertices = 0;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 38:
entity.elevation = curr.value as number;
break;
case 39:
entity.depth = curr.value as number;
break;
case 70: // 1 = Closed shape, 128 = plinegen?, 0 = default
entity.shape = (((curr.value as number) & 1) === 1);
entity.hasContinuousLinetypePattern = (((curr.value as number) & 128) === 128);
break;
case 90:
numberOfVertices = curr.value as number;
break;
case 10: // X coordinate of point
entity.vertices = parseLWPolylineVertices(numberOfVertices, scanner);
break;
case 43:
if (curr.value !== 0) entity.width = curr.value as number;
break;
case 210:
entity.extrusionDirectionX = curr.value as number;
break;
case 220:
entity.extrusionDirectionY = curr.value as number;
break;
case 230:
entity.extrusionDirectionZ = curr.value as number;
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
function parseLWPolylineVertices(n:number, scanner: DxfArrayScanner) {
if (!n || n <= 0) throw Error('n must be greater than 0 verticies');
const vertices = [] as IVertex[];
let vertexIsStarted = false;
let vertexIsFinished = false;
let curr = scanner.lastReadGroup as IGroup;
for (let i = 0; i < n; i++) {
const vertex = {} as IVertex;
while (!scanner.isEOF()) {
if (curr.code === 0 || vertexIsFinished) break;
switch (curr.code) {
case 10: // X
if (vertexIsStarted) {
vertexIsFinished = true;
continue;
}
vertex.x = curr.value as number;
vertexIsStarted = true;
break;
case 20: // Y
vertex.y = curr.value as number;
break;
case 30: // Z
vertex.z = curr.value as number;
break;
case 40: // start width
vertex.startWidth = curr.value as number;
break;
case 41: // end width
vertex.endWidth = curr.value as number;
break;
case 42: // bulge
if (curr.value != 0) vertex.bulge = curr.value as number;
break;
default:
// if we do not hit known code return vertices. Code might belong to entity
scanner.rewind();
if (vertexIsStarted) {
vertices.push(vertex);
}
scanner.rewind();
return vertices;
}
curr = scanner.next();
}
// See https://groups.google.com/forum/#!topic/comp.cad.autocad/9gn8s5O_w6E
vertices.push(vertex);
vertexIsStarted = false;
vertexIsFinished = false;
}
scanner.rewind();
return vertices;
}
@@ -0,0 +1,62 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IMtextEntity extends IEntity {
text: string;
position: IPoint;
directionVector: IPoint;
height: number;
width: number;
rotation: number;
attachmentPoint: number;
drawingDirection: number;
}
export default class Mtext implements IGeometry {
public ForEntityName = 'MTEXT' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value } as IMtextEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 3:
entity.text ? entity.text += curr.value : entity.text = curr.value as string;
break;
case 1:
entity.text ? entity.text += curr.value : entity.text = curr.value as string;
break;
case 10:
entity.position = helpers.parsePoint(scanner);
break;
case 11:
entity.directionVector = helpers.parsePoint(scanner);
break;
case 40:
//Note: this is the text height
entity.height = curr.value as number;
break;
case 41:
entity.width = curr.value as number;
break;
case 50:
entity.rotation = curr.value as number;
break;
case 71:
entity.attachmentPoint = curr.value as number;
break;
case 72:
entity.drawingDirection = curr.value as number;
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,42 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IPointEntity extends IEntity{
position: IPoint;
thickness: number;
extrusionDirection: IPoint;
}
export default class Point implements IGeometry{
public ForEntityName= 'POINT' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const type = curr.value as string;
const entity = { type } as unknown as IPointEntity;
curr = scanner.next();
while(!scanner.isEOF()) {
if(curr.code === 0) break;
switch(curr.code) {
case 10:
entity.position = helpers.parsePoint(scanner);
break;
case 39:
entity.thickness = curr.value as number;
break;
case 210:
entity.extrusionDirection = helpers.parsePoint(scanner);
break;
case 100:
break;
default: // check common entity attributes
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,107 @@
import * as helpers from '../ParseHelpers'
import VertexParser, { IVertexEntity } from './vertex';
import IGeometry, { IEntity, IPoint } from './geomtry';
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
export interface IPolylineEntity extends IEntity {
vertices: IVertexEntity[];
thickness: number;
shape: boolean;
includesCurveFitVertices: boolean;
includesSplineFitVertices: boolean;
is3dPolyline: boolean;
is3dPolygonMesh: boolean;
is3dPolygonMeshClosed: boolean;
isPolyfaceMesh: boolean;
hasContinuousLinetypePattern: boolean;
extrusionDirection: IPoint;
}
export default class Polyline implements IGeometry {
public ForEntityName = 'POLYLINE' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
var entity = { type: curr.value, vertices: [] as IVertexEntity[] } as IPolylineEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 10: // always 0
break;
case 20: // always 0
break;
case 30: // elevation
break;
case 39: // thickness
entity.thickness = curr.value as number;
break;
case 40: // start width
break;
case 41: // end width
break;
case 70:
entity.shape = ((curr.value as number) & 1) !== 0;
entity.includesCurveFitVertices = ((curr.value as number) & 2) !== 0;
entity.includesSplineFitVertices = ((curr.value as number) & 4) !== 0;
entity.is3dPolyline = ((curr.value as number) & 8) !== 0;
entity.is3dPolygonMesh = ((curr.value as number) & 16) !== 0;
entity.is3dPolygonMeshClosed = ((curr.value as number) & 32) !== 0; // 32 = The polygon mesh is closed in the N direction
entity.isPolyfaceMesh = ((curr.value as number) & 64) !== 0;
entity.hasContinuousLinetypePattern = ((curr.value as number) & 128) !== 0;
break;
case 71: // Polygon mesh M vertex count
break;
case 72: // Polygon mesh N vertex count
break;
case 73: // Smooth surface M density
break;
case 74: // Smooth surface N density
break;
case 75: // Curves and smooth surface type
break;
case 210:
entity.extrusionDirection = helpers.parsePoint(scanner);
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
entity.vertices = parsePolylineVertices(scanner, curr);
return entity;
}
}
function parsePolylineVertices(scanner:DxfArrayScanner, curr:IGroup) {
const vertexParser = new VertexParser();
const vertices: IVertexEntity[] = [];
while (!scanner.isEOF()) {
if (curr.code === 0) {
if (curr.value === 'VERTEX') {
vertices.push(vertexParser.parseEntity(scanner, curr));
curr = scanner.lastReadGroup as IGroup;
} else if (curr.value === 'SEQEND') {
parseSeqEnd(scanner, curr);
break;
}
}
}
return vertices;
}
function parseSeqEnd(scanner:DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value } as IEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code == 0) break;
helpers.checkCommonEntityProperties(entity, curr, scanner);
curr = scanner.next();
}
return entity;
};
@@ -0,0 +1,44 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface ISolidEntity extends IEntity {
points: IPoint[];
extrusionDirection: IPoint;
}
export default class Solid implements IGeometry {
public ForEntityName = 'SOLID' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value, points: [] as IPoint[] } as ISolidEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 10:
entity.points[0] = helpers.parsePoint(scanner);
break;
case 11:
entity.points[1] = helpers.parsePoint(scanner);
break;
case 12:
entity.points[2] = helpers.parsePoint(scanner);
break;
case 13:
entity.points[3] = helpers.parsePoint(scanner);
break;
case 210:
entity.extrusionDirection = helpers.parsePoint(scanner);
break;
default: // check common entity attributes
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,86 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface ISplineEntity extends IEntity {
controlPoints?: IPoint[];
fitPoints?: IPoint[];
startTangent: IPoint;
endTangent: IPoint;
knotValues: number[];
closed: boolean;
periodic: boolean;
rational: boolean;
planar: boolean;
linear: boolean;
degreeOfSplineCurve: number;
numberOfKnots: number;
numberOfControlPoints: number;
numberOfFitPoints: number;
normalVector: IPoint;
}
export default class Spline implements IGeometry {
public ForEntityName = 'SPLINE' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = { type: curr.value } as ISplineEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 10:
if (!entity.controlPoints) entity.controlPoints = [];
entity.controlPoints.push(helpers.parsePoint(scanner));
break;
case 11:
if (!entity.fitPoints) entity.fitPoints = [];
entity.fitPoints.push(helpers.parsePoint(scanner));
break;
case 12:
entity.startTangent = helpers.parsePoint(scanner);
break;
case 13:
entity.endTangent = helpers.parsePoint(scanner);
break;
case 40:
if (!entity.knotValues) entity.knotValues = [];
entity.knotValues.push(curr.value as number);
break;
case 70:
if (((curr.value as number) & 1) != 0) entity.closed = true;
if (((curr.value as number) & 2) != 0) entity.periodic = true;
if (((curr.value as number) & 4) != 0) entity.rational = true;
if (((curr.value as number) & 8) != 0) entity.planar = true;
if (((curr.value as number) & 16) != 0) {
entity.planar = true;
entity.linear = true;
}
break;
case 71:
entity.degreeOfSplineCurve = curr.value as number;
break;
case 72:
entity.numberOfKnots = curr.value as number;
break;
case 73:
entity.numberOfControlPoints = curr.value as number;
break;
case 74:
entity.numberOfFitPoints = curr.value as number;
break;
case 210:
entity.normalVector = helpers.parsePoint(scanner);
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,96 @@
import DxfArrayScanner, {IGroup} from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, {IEntity, IPoint} from './geomtry';
export interface ITextEntity extends IEntity {
startPoint: IPoint;
endPoint: IPoint;
textHeight: number;
xScale: number;
rotation: number;
obliqueAngle: number;
text: string;
fontName: string;
thickness: number;
halign: number;
valign: number;
flags: number;
}
export default class Text implements IGeometry {
public ForEntityName = 'TEXT' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
const entity = {type: curr.value} as ITextEntity;
curr = scanner.next();
while (!scanner.isEOF()) {
if (curr.code === 0) break;
switch (curr.code) {
case 1:
// 字符串本身
entity.text = curr.value as string;
break;
case 7: // 字体名称/文字样式名(可选;默认值 = 标准)
entity.fontName = curr.value as string;
break;
case 10:
// 第一对齐点(在 OCS 中) DXF:X 值;APP:三维点
entity.startPoint = helpers.parsePoint(scanner);
break;
case 11:
// 第二对齐点(在 OCS 中)(可选);
// DXFX 值;APP:三维点;
// 只有当 72 或 73 组的值非零时,该值才有意义(如果对正不是基线对正/左对正)
entity.endPoint = helpers.parsePoint(scanner);
break;
case 39:
// 厚度(可选;默认值 = 0
entity.thickness = curr.value as number;
break;
case 40:
// 文字高度
entity.textHeight = curr.value as number;
break;
case 41:
// 相对 X 比例因子 — 宽度(可选;默认值 = 1)
// 使用拟合类型的文字时,该值也将进行调整。
entity.xScale = curr.value as number;
break;
case 50:
// 文字旋转角度(可选;默认值 = 0)
entity.rotation = curr.value as number;
break;
case 51:
// 倾斜角度(可选;默认值 = 0)
entity.obliqueAngle = curr.value as number;
break;
case 71:
// 文字生成标志(可选,仅适用于 SHX 文字):
// 2 = 文字反向(在 X 轴方向镜像)
// 4 = 文字倒置(在 Y 轴方向镜像)
entity.flags = curr.value as number;
break;
// NOTE: 72和73没有11就没有意义(第二个对齐点)
case 72:
// 文字水平对正类型(可选;默认值 = 0)整数代码(非按位编码)
// 0 = 左对正;1 = 居中对正;2 = 右对正
// 3 = 对齐(如果垂直对齐 = 0)
// 4 = 中间(如果垂直对齐 = 0)
// 5 = 拟合(如果垂直对齐 = 0)
entity.halign = curr.value as number;
break;
case 73:
// 文字垂直对正类型(可选;默认值 = 0)整数代码(不是按位编码)
// 0 = 基线对正;1 = 底端对正;2 = 居中对正;3 = 顶端对正
entity.valign = curr.value as number;
break;
default:
// check common entity attributes
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
@@ -0,0 +1,79 @@
import DxfArrayScanner, { IGroup } from '../DxfArrayScanner';
import * as helpers from '../ParseHelpers'
import IGeometry, { IEntity, IPoint } from './geomtry';
export interface IVertexEntity extends IEntity, IPoint{
bulge: number;
curveFittingVertex: boolean;
curveFitTangent: boolean;
splineVertex: boolean;
splineControlPoint: boolean;
threeDPolylineVertex: boolean;
threeDPolylineMesh: boolean;
polyfaceMeshVertex: boolean;
faceA: number;
faceB: number;
faceC: number;
faceD: number;
}
export default class Vertex implements IGeometry {
public ForEntityName= 'VERTEX' as const;
public parseEntity(scanner: DxfArrayScanner, curr: IGroup) {
var entity = { type: curr.value } as IVertexEntity;
curr = scanner.next();
while(!scanner.isEOF()) {
if(curr.code === 0) break;
switch(curr.code) {
case 10: // X
entity.x = curr.value as number;
break;
case 20: // Y
entity.y = curr.value as number;
break;
case 30: // Z
entity.z = curr.value as number;
break;
case 40: // start width
break;
case 41: // end width
break;
case 42: // bulge
if(curr.value != 0) entity.bulge = curr.value as number;
break;
case 70: // flags
entity.curveFittingVertex = ((curr.value as number) & 1) !== 0;
entity.curveFitTangent = ((curr.value as number) & 2) !== 0;
entity.splineVertex = ((curr.value as number) & 8) !== 0;
entity.splineControlPoint = ((curr.value as number) & 16) !== 0;
entity.threeDPolylineVertex = ((curr.value as number) & 32) !== 0;
entity.threeDPolylineMesh = ((curr.value as number) & 64) !== 0;
entity.polyfaceMeshVertex = ((curr.value as number) & 128) !== 0;
break;
case 50: // curve fit tangent direction
break;
case 71: // polyface mesh vertex index
entity.faceA = curr.value as number;
break;
case 72: // polyface mesh vertex index
entity.faceB = curr.value as number;
break;
case 73: // polyface mesh vertex index
entity.faceC = curr.value as number;
break;
case 74: // polyface mesh vertex index
entity.faceD = curr.value as number;
break;
default:
helpers.checkCommonEntityProperties(entity, curr, scanner);
break;
}
curr = scanner.next();
}
return entity;
}
}
+22
View File
@@ -0,0 +1,22 @@
import DxfParser from './DxfParser';
export { default as DxfParser } from './DxfParser';
export type { IDxf, IBlock, ILayerTypesTable, ILayersTable, ITables, IViewPortTable, IBaseTable, ILayer, ILayerTableDefinition, ILineType, ILineTypeTableDefinition, ITable, ITableDefinitions, IViewPort, IViewPortTableDefinition } from './DxfParser';
export type { IEntity, IPoint } from './entities/geomtry';
export type { I3DfaceEntity } from './entities/3dface';
export type { IArcEntity } from './entities/arc';
export type { IAttdefEntity } from './entities/attdef';
export type { ICircleEntity } from './entities/circle';
export type { IDimensionEntity } from './entities/dimension';
export type { IEllipseEntity } from './entities/ellipse';
export type { IInsertEntity } from './entities/insert';
export type { ILineEntity } from './entities/line';
export type { ILwpolylineEntity } from './entities/lwpolyline';
export type { IMtextEntity } from './entities/mtext';
export type { IPointEntity } from './entities/point';
export type { IPolylineEntity } from './entities/polyline';
export type { ISolidEntity } from './entities/solid';
export type { ISplineEntity } from './entities/spline';
export type { ITextEntity } from './entities/text';
export type { IVertexEntity } from './entities/vertex';
export default DxfParser
+64
View File
@@ -0,0 +1,64 @@
import { Raycaster, Mesh, Group, Vector2, Scene, OrthographicCamera,Color } from "three";
export class PickHelper {
private raycaster: Raycaster;
private pickedObject: Mesh | Group | undefined;
private objects: Mesh[];
private camera: OrthographicCamera;
private scene: Scene;
private bloomLayer: number;
private pickedColor = new Color("#63E2B7");
private lastPickedColor = null;
constructor(scene: Scene, camera: OrthographicCamera, bloomLayer: number) {
this.camera = camera;
this.scene = scene;
this.bloomLayer = bloomLayer;
this.raycaster = new Raycaster();
this.objects = [];
this.scene.traverseVisible(child => {
if (child.children.length == 0) {
this.objects.push(<Mesh>child);
}
});
}
pick(normalizedPosition: Vector2,) {
// 透过截锥投射一道光线
this.raycaster.setFromCamera(normalizedPosition, this.camera);
// 获取射线相交的物体列表
const intersectedObjects = this.raycaster.intersectObjects(this.objects, false);
if (intersectedObjects.length) {
// 选择第一个对象。这是最近的一个
this.select(intersectedObjects[0].object as Mesh);
}
}
select(object: Mesh | Group) {
if (!object || object.uuid === this.pickedObject?.uuid) return;
// 如果有被选中的物体,则恢复
if (this.pickedObject) {
this.pickedObject.layers.toggle(this.bloomLayer);
if(this.pickedObject.material && this.lastPickedColor){
this.pickedObject.material.color = this.lastPickedColor;
this.lastPickedColor = null;
}
this.pickedObject = undefined;
}
this.pickedObject = object;
this.pickedObject.layers.toggle(this.bloomLayer);
if(this.pickedObject.material){
this.lastPickedColor = this.pickedObject.material.color;
this.pickedObject.material.color = this.pickedColor;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// 这是基础的示例代码
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor
// MDN上的示例代码是公共领域或CC0(您的偏好)或MIT,具体取决于示例代码添加的时间:
// https://developer.mozilla.org/en-US/docs/MDN/About
export default (value: number | string[], exp?:number | undefined) => {
// 如果exp没有定义或者为零...
if (typeof exp === 'undefined' || +exp === 0) {
return Math.round(value as number)
}
value = +value
exp = +exp
// 如果值不是数字或exp不是整数…
if (isNaN(value) || !(exp % 1 === 0)) {
return NaN
}
// Shift
value = value.toString().split('e')
value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)))
// Shift back
value = value.toString().split('e')
return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,728 @@
import { ShaderChunk, UniformsUtils, MeshDepthMaterial, RGBADepthPacking, MeshDistanceMaterial, ShaderLib, Matrix4, Vector3, Mesh, CylinderGeometry as CylinderBufferGeometry, Vector2, MeshStandardMaterial, DoubleSide } from 'three';
/**
* Regular expression for matching the `void main() {` opener line in GLSL.
* @type {RegExp}
*/
const voidMainRegExp = /\bvoid\s+main\s*\(\s*\)\s*{/g;
/**
* Recursively expands all `#include <xyz>` statements within string of shader code.
* Copied from three's WebGLProgram#parseIncludes for external use.
*
* @param {string} source - The GLSL source code to evaluate
* @return {string} The GLSL code with all includes expanded
*/
function expandShaderIncludes( source ) {
const pattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function replace(match, include) {
let chunk = ShaderChunk[include];
return chunk ? expandShaderIncludes(chunk) : match
}
return source.replace( pattern, replace )
}
/*
* This is a direct copy of MathUtils.generateUUID from Three.js, to preserve compatibility with three
* versions before 0.113.0 as it was changed from Math to MathUtils in that version.
* https://github.com/mrdoob/three.js/blob/dd8b5aa3b270c17096b90945cd2d6d1b13aaec53/src/math/MathUtils.js#L16
*/
const _lut = [];
for (let i = 0; i < 256; i++) {
_lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
}
function generateUUID() {
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
const d0 = Math.random() * 0xffffffff | 0;
const d1 = Math.random() * 0xffffffff | 0;
const d2 = Math.random() * 0xffffffff | 0;
const d3 = Math.random() * 0xffffffff | 0;
const uuid = _lut[d0 & 0xff] + _lut[d0 >> 8 & 0xff] + _lut[d0 >> 16 & 0xff] + _lut[d0 >> 24 & 0xff] + '-' +
_lut[d1 & 0xff] + _lut[d1 >> 8 & 0xff] + '-' + _lut[d1 >> 16 & 0x0f | 0x40] + _lut[d1 >> 24 & 0xff] + '-' +
_lut[d2 & 0x3f | 0x80] + _lut[d2 >> 8 & 0xff] + '-' + _lut[d2 >> 16 & 0xff] + _lut[d2 >> 24 & 0xff] +
_lut[d3 & 0xff] + _lut[d3 >> 8 & 0xff] + _lut[d3 >> 16 & 0xff] + _lut[d3 >> 24 & 0xff];
// .toUpperCase() here flattens concatenated strings to save heap memory space.
return uuid.toUpperCase()
}
// Local assign polyfill to avoid importing troika-core
const assign = Object.assign || function(/*target, ...sources*/) {
let target = arguments[0];
for (let i = 1, len = arguments.length; i < len; i++) {
let source = arguments[i];
if (source) {
for (let prop in source) {
if (source.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
}
}
return target
};
const epoch = Date.now();
const CONSTRUCTOR_CACHE = new WeakMap();
const SHADER_UPGRADE_CACHE = new Map();
// Material ids must be integers, but we can't access the increment from Three's `Material` module,
// so let's choose a sufficiently large starting value that should theoretically never collide.
let materialInstanceId = 1e10;
/**
* A utility for creating a custom shader material derived from another material's
* shaders. This allows you to inject custom shader logic and transforms into the
* builtin ThreeJS materials without having to recreate them from scratch.
*
* @param {THREE.Material} baseMaterial - the original material to derive from
*
* @param {Object} options - How the base material should be modified.
* @param {Object} options.defines - Custom `defines` for the material
* @param {Object} options.extensions - Custom `extensions` for the material, e.g. `{derivatives: true}`
* @param {Object} options.uniforms - Custom `uniforms` for use in the modified shader. These can
* be accessed and manipulated via the resulting material's `uniforms` property, just like
* in a ShaderMaterial. You do not need to repeat the base material's own uniforms here.
* @param {String} options.timeUniform - If specified, a uniform of this name will be injected into
* both shaders, and it will automatically be updated on each render frame with a number of
* elapsed milliseconds. The "zero" epoch time is not significant so don't rely on this as a
* true calendar time.
* @param {String} options.vertexDefs - Custom GLSL code to inject into the vertex shader's top-level
* definitions, above the `void main()` function.
* @param {String} options.vertexMainIntro - Custom GLSL code to inject at the top of the vertex
* shader's `void main` function.
* @param {String} options.vertexMainOutro - Custom GLSL code to inject at the end of the vertex
* shader's `void main` function.
* @param {String} options.vertexTransform - Custom GLSL code to manipulate the `position`, `normal`,
* and/or `uv` vertex attributes. This code will be wrapped within a standalone function with
* those attributes exposed by their normal names as read/write values.
* @param {String} options.fragmentDefs - Custom GLSL code to inject into the fragment shader's top-level
* definitions, above the `void main()` function.
* @param {String} options.fragmentMainIntro - Custom GLSL code to inject at the top of the fragment
* shader's `void main` function.
* @param {String} options.fragmentMainOutro - Custom GLSL code to inject at the end of the fragment
* shader's `void main` function. You can manipulate `gl_FragColor` here but keep in mind it goes
* after any of ThreeJS's color postprocessing shader chunks (tonemapping, fog, etc.), so if you
* want those to apply to your changes use `fragmentColorTransform` instead.
* @param {String} options.fragmentColorTransform - Custom GLSL code to manipulate the `gl_FragColor`
* output value. Will be injected near the end of the `void main` function, but before any
* of ThreeJS's color postprocessing shader chunks (tonemapping, fog, etc.), and before the
* `fragmentMainOutro`.
* @param {function<{vertexShader,fragmentShader}>:{vertexShader,fragmentShader}} options.customRewriter - A function
* for performing custom rewrites of the full shader code. Useful if you need to do something
* special that's not covered by the other builtin options. This function will be executed before
* any other transforms are applied.
* @param {boolean} options.chained - Set to `true` to prototype-chain the derived material to the base
* material, rather than the default behavior of copying it. This allows the derived material to
* automatically pick up changes made to the base material and its properties. This can be useful
* where the derived material is hidden from the user as an implementation detail, allowing them
* to work with the original material like normal. But it can result in unexpected behavior if not
* handled carefully.
*
* @return {THREE.Material}
*
* The returned material will also have two new methods, `getDepthMaterial()` and `getDistanceMaterial()`,
* which can be called to get a variant of the derived material for use in shadow casting. If the
* target mesh is expected to cast shadows, then you can assign these to the mesh's `customDepthMaterial`
* (for directional and spot lights) and/or `customDistanceMaterial` (for point lights) properties to
* allow the cast shadow to honor your derived shader's vertex transforms and discarded fragments. These
* will also set a custom `#define IS_DEPTH_MATERIAL` or `#define IS_DISTANCE_MATERIAL` that you can look
* for in your derived shaders with `#ifdef` to customize their behavior for the depth or distance
* scenarios, e.g. skipping antialiasing or expensive shader logic.
*/
function createDerivedMaterial(baseMaterial, options) {
// Generate a key that is unique to the content of these `options`. We'll use this
// throughout for caching and for generating the upgraded shader code. This increases
// the likelihood that the resulting shaders will line up across multiple calls so
// their GL programs can be shared and cached.
const optionsKey = getKeyForOptions(options);
// First check to see if we've already derived from this baseMaterial using this
// unique set of options, and if so reuse the constructor to avoid some allocations.
let ctorsByDerivation = CONSTRUCTOR_CACHE.get(baseMaterial);
if (!ctorsByDerivation) {
CONSTRUCTOR_CACHE.set(baseMaterial, (ctorsByDerivation = Object.create(null)));
}
if (ctorsByDerivation[optionsKey]) {
return new ctorsByDerivation[optionsKey]()
}
const privateBeforeCompileProp = `_onBeforeCompile${optionsKey}`;
// Private onBeforeCompile handler that injects the modified shaders and uniforms when
// the renderer switches to this material's program
const onBeforeCompile = function (shaderInfo) {
baseMaterial.onBeforeCompile.call(this, shaderInfo);
// Upgrade the shaders, caching the result by incoming source code
const cacheKey = optionsKey + '|||' + shaderInfo.vertexShader + '|||' + shaderInfo.fragmentShader;
let upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey];
if (!upgradedShaders) {
const upgraded = upgradeShaders(shaderInfo, options, optionsKey);
upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey] = upgraded;
}
// Inject upgraded shaders and uniforms into the program
shaderInfo.vertexShader = upgradedShaders.vertexShader;
shaderInfo.fragmentShader = upgradedShaders.fragmentShader;
assign(shaderInfo.uniforms, this.uniforms);
// Inject auto-updating time uniform if requested
if (options.timeUniform) {
shaderInfo.uniforms[options.timeUniform] = {
get value() {return Date.now() - epoch}
};
}
// Users can still add their own handlers on top of ours
if (this[privateBeforeCompileProp]) {
this[privateBeforeCompileProp](shaderInfo);
}
};
const DerivedMaterial = function DerivedMaterial() {
return derive(options.chained ? baseMaterial : baseMaterial.clone())
};
const derive = function(base) {
// Prototype chain to the base material
const derived = Object.create(base, descriptor);
// Store the baseMaterial for reference; this is always the original even when cloning
Object.defineProperty(derived, 'baseMaterial', { value: baseMaterial });
// Needs its own ids
Object.defineProperty(derived, 'id', { value: materialInstanceId++ });
derived.uuid = generateUUID();
// Merge uniforms, defines, and extensions
derived.uniforms = assign({}, base.uniforms, options.uniforms);
derived.defines = assign({}, base.defines, options.defines);
derived.defines[`TROIKA_DERIVED_MATERIAL_${optionsKey}`] = ''; //force a program change from the base material
derived.extensions = assign({}, base.extensions, options.extensions);
// Don't inherit EventDispatcher listeners
derived._listeners = undefined;
return derived
};
const descriptor = {
constructor: {value: DerivedMaterial},
isDerivedMaterial: {value: true},
customProgramCacheKey: {
writable: true,
configurable: true,
value: function () {
return optionsKey
}
},
onBeforeCompile: {
get() {
return onBeforeCompile
},
set(fn) {
this[privateBeforeCompileProp] = fn;
}
},
copy: {
writable: true,
configurable: true,
value: function (source) {
baseMaterial.copy.call(this, source);
if (!baseMaterial.isShaderMaterial && !baseMaterial.isDerivedMaterial) {
assign(this.extensions, source.extensions);
assign(this.defines, source.defines);
assign(this.uniforms, UniformsUtils.clone(source.uniforms));
}
return this
}
},
clone: {
writable: true,
configurable: true,
value: function () {
const newBase = new baseMaterial.constructor();
return derive(newBase).copy(this)
}
},
/**
* Utility to get a MeshDepthMaterial that will honor this derived material's vertex
* transformations and discarded fragments.
*/
getDepthMaterial: {
writable: true,
configurable: true,
value: function() {
let depthMaterial = this._depthMaterial;
if (!depthMaterial) {
depthMaterial = this._depthMaterial = createDerivedMaterial(
baseMaterial.isDerivedMaterial
? baseMaterial.getDepthMaterial()
: new MeshDepthMaterial({ depthPacking: RGBADepthPacking }),
options
);
depthMaterial.defines.IS_DEPTH_MATERIAL = '';
depthMaterial.uniforms = this.uniforms; //automatically recieve same uniform values
}
return depthMaterial
}
},
/**
* Utility to get a MeshDistanceMaterial that will honor this derived material's vertex
* transformations and discarded fragments.
*/
getDistanceMaterial: {
writable: true,
configurable: true,
value: function() {
let distanceMaterial = this._distanceMaterial;
if (!distanceMaterial) {
distanceMaterial = this._distanceMaterial = createDerivedMaterial(
baseMaterial.isDerivedMaterial
? baseMaterial.getDistanceMaterial()
: new MeshDistanceMaterial(),
options
);
distanceMaterial.defines.IS_DISTANCE_MATERIAL = '';
distanceMaterial.uniforms = this.uniforms; //automatically recieve same uniform values
}
return distanceMaterial
}
},
dispose: {
writable: true,
configurable: true,
value() {
const {_depthMaterial, _distanceMaterial} = this;
if (_depthMaterial) _depthMaterial.dispose();
if (_distanceMaterial) _distanceMaterial.dispose();
baseMaterial.dispose.call(this);
}
}
};
ctorsByDerivation[optionsKey] = DerivedMaterial;
return new DerivedMaterial()
}
function upgradeShaders({vertexShader, fragmentShader}, options, key) {
let {
vertexDefs,
vertexMainIntro,
vertexMainOutro,
vertexTransform,
fragmentDefs,
fragmentMainIntro,
fragmentMainOutro,
fragmentColorTransform,
customRewriter,
timeUniform
} = options;
vertexDefs = vertexDefs || '';
vertexMainIntro = vertexMainIntro || '';
vertexMainOutro = vertexMainOutro || '';
fragmentDefs = fragmentDefs || '';
fragmentMainIntro = fragmentMainIntro || '';
fragmentMainOutro = fragmentMainOutro || '';
// Expand includes if needed
if (vertexTransform || customRewriter) {
vertexShader = expandShaderIncludes(vertexShader);
}
if (fragmentColorTransform || customRewriter) {
// We need to be able to find postprocessing chunks after include expansion in order to
// put them after the fragmentColorTransform, so mark them with comments first. Even if
// this particular derivation doesn't have a fragmentColorTransform, other derivations may,
// so we still mark them.
fragmentShader = fragmentShader.replace(
/^[ \t]*#include <((?:tonemapping|encodings|fog|premultiplied_alpha|dithering)_fragment)>/gm,
'\n//!BEGIN_POST_CHUNK $1\n$&\n//!END_POST_CHUNK\n'
);
fragmentShader = expandShaderIncludes(fragmentShader);
}
// Apply custom rewriter function
if (customRewriter) {
let res = customRewriter({vertexShader, fragmentShader});
vertexShader = res.vertexShader;
fragmentShader = res.fragmentShader;
}
// The fragmentColorTransform needs to go before any postprocessing chunks, so extract
// those and re-insert them into the outro in the correct place:
if (fragmentColorTransform) {
let postChunks = [];
fragmentShader = fragmentShader.replace(
/^\/\/!BEGIN_POST_CHUNK[^]+?^\/\/!END_POST_CHUNK/gm, // [^]+? = non-greedy match of any chars including newlines
match => {
postChunks.push(match);
return ''
}
);
fragmentMainOutro = `${fragmentColorTransform}\n${postChunks.join('\n')}\n${fragmentMainOutro}`;
}
// Inject auto-updating time uniform if requested
if (timeUniform) {
const code = `\nuniform float ${timeUniform};\n`;
vertexDefs = code + vertexDefs;
fragmentDefs = code + fragmentDefs;
}
// Inject a function for the vertexTransform and rename all usages of position/normal/uv
if (vertexTransform) {
// Hoist these defs to the very top so they work in other function defs
vertexShader = `vec3 troika_position_${key};
vec3 troika_normal_${key};
vec2 troika_uv_${key};
${vertexShader}
`;
vertexDefs = `${vertexDefs}
void troikaVertexTransform${key}(inout vec3 position, inout vec3 normal, inout vec2 uv) {
${vertexTransform}
}
`;
vertexMainIntro = `
troika_position_${key} = vec3(position);
troika_normal_${key} = vec3(normal);
troika_uv_${key} = vec2(uv);
troikaVertexTransform${key}(troika_position_${key}, troika_normal_${key}, troika_uv_${key});
${vertexMainIntro}
`;
vertexShader = vertexShader.replace(/\b(position|normal|uv)\b/g, (match, match1, index, fullStr) => {
return /\battribute\s+vec[23]\s+$/.test(fullStr.substr(0, index)) ? match1 : `troika_${match1}_${key}`
});
}
// Inject defs and intro/outro snippets
vertexShader = injectIntoShaderCode(vertexShader, key, vertexDefs, vertexMainIntro, vertexMainOutro);
fragmentShader = injectIntoShaderCode(fragmentShader, key, fragmentDefs, fragmentMainIntro, fragmentMainOutro);
return {
vertexShader,
fragmentShader
}
}
function injectIntoShaderCode(shaderCode, id, defs, intro, outro) {
if (intro || outro || defs) {
shaderCode = shaderCode.replace(voidMainRegExp, `
${defs}
void troikaOrigMain${id}() {`
);
shaderCode += `
void main() {
${intro}
troikaOrigMain${id}();
${outro}
}`;
}
return shaderCode
}
function optionsJsonReplacer(key, value) {
return key === 'uniforms' ? undefined : typeof value === 'function' ? value.toString() : value
}
let _idCtr = 0;
const optionsHashesToIds = new Map();
function getKeyForOptions(options) {
const optionsHash = JSON.stringify(options, optionsJsonReplacer);
let id = optionsHashesToIds.get(optionsHash);
if (id == null) {
optionsHashesToIds.set(optionsHash, (id = ++_idCtr));
}
return id
}
// Copied from threejs WebGLPrograms.js so we can resolve builtin materials to their shaders
// TODO how can we keep this from getting stale?
const MATERIAL_TYPES_TO_SHADERS = {
MeshDepthMaterial: 'depth',
MeshDistanceMaterial: 'distanceRGBA',
MeshNormalMaterial: 'normal',
MeshBasicMaterial: 'basic',
MeshLambertMaterial: 'lambert',
MeshPhongMaterial: 'phong',
MeshToonMaterial: 'toon',
MeshStandardMaterial: 'physical',
MeshPhysicalMaterial: 'physical',
MeshMatcapMaterial: 'matcap',
LineBasicMaterial: 'basic',
LineDashedMaterial: 'dashed',
PointsMaterial: 'points',
ShadowMaterial: 'shadow',
SpriteMaterial: 'sprite'
};
/**
* Given a Three.js `Material` instance, find the shaders/uniforms that will be
* used to render that material.
*
* @param material - the Material instance
* @return {object} - the material's shader info: `{uniforms:{}, fragmentShader:'', vertexShader:''}`
*/
function getShadersForMaterial(material) {
let builtinType = MATERIAL_TYPES_TO_SHADERS[material.type];
return builtinType ? ShaderLib[builtinType] : material //TODO fallback for unknown type?
}
/**
* Find all uniforms and their types within a shader code string.
*
* @param {string} shader - The shader code to parse
* @return {object} mapping of uniform names to their glsl type
*/
function getShaderUniformTypes(shader) {
let uniformRE = /\buniform\s+(int|float|vec[234])\s+([A-Za-z_][\w]*)/g;
let uniforms = Object.create(null);
let match;
while ((match = uniformRE.exec(shader)) !== null) {
uniforms[match[2]] = match[1];
}
return uniforms
}
/**
* Helper for smoothing out the `m.getInverse(x)` --> `m.copy(x).invert()` conversion
* that happened in ThreeJS r123.
* @param {Matrix4} srcMatrix
* @param {Matrix4} [tgtMatrix]
*/
function invertMatrix4(srcMatrix, tgtMatrix = new Matrix4()) {
if (typeof tgtMatrix.invert === 'function') {
tgtMatrix.copy(srcMatrix).invert();
} else {
tgtMatrix.getInverse(srcMatrix);
}
return tgtMatrix
}
/*
Input geometry is a cylinder with r=1, height in y dimension from 0 to 1,
divided into a reasonable number of height segments.
*/
const vertexDefs = `
uniform vec3 pointA;
uniform vec3 controlA;
uniform vec3 controlB;
uniform vec3 pointB;
uniform float radius;
varying float bezierT;
vec3 cubicBezier(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {
float t2 = 1.0 - t;
float b0 = t2 * t2 * t2;
float b1 = 3.0 * t * t2 * t2;
float b2 = 3.0 * t * t * t2;
float b3 = t * t * t;
return b0 * p1 + b1 * c1 + b2 * c2 + b3 * p2;
}
vec3 cubicBezierDerivative(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {
float t2 = 1.0 - t;
return -3.0 * p1 * t2 * t2 +
c1 * (3.0 * t2 * t2 - 6.0 * t2 * t) +
c2 * (6.0 * t2 * t - 3.0 * t * t) +
3.0 * p2 * t * t;
}
`;
const vertexTransform = `
float t = position.y;
bezierT = t;
vec3 bezierCenterPos = cubicBezier(pointA, controlA, controlB, pointB, t);
vec3 bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t));
// Make "sideways" always perpendicular to the camera ray; this ensures that any twists
// in the cylinder occur where you won't see them:
vec3 viewDirection = normalMatrix * vec3(0.0, 0.0, 1.0);
if (bezierDir == viewDirection) {
bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t == 1.0 ? t - 0.0001 : t + 0.0001));
}
vec3 sideways = normalize(cross(bezierDir, viewDirection));
vec3 upish = normalize(cross(sideways, bezierDir));
// Build a matrix for transforming this disc in the cylinder:
mat4 discTx;
discTx[0].xyz = sideways * radius;
discTx[1].xyz = bezierDir * radius;
discTx[2].xyz = upish * radius;
discTx[3].xyz = bezierCenterPos;
discTx[3][3] = 1.0;
// Apply transform, ignoring original y
position = (discTx * vec4(position.x, 0.0, position.z, 1.0)).xyz;
normal = normalize(mat3(discTx) * normal);
`;
const fragmentDefs = `
uniform vec3 dashing;
varying float bezierT;
`;
const fragmentMainIntro = `
if (dashing.x + dashing.y > 0.0) {
float dashFrac = mod(bezierT - dashing.z, dashing.x + dashing.y);
if (dashFrac > dashing.x) {
discard;
}
}
`;
// Debugging: separate color for each of the 6 sides:
// const fragmentColorTransform = `
// float sideNum = floor(vUV.x * 6.0);
// vec3 mixColor = sideNum < 1.0 ? vec3(1.0, 0.0, 0.0) :
// sideNum < 2.0 ? vec3(0.0, 1.0, 1.0) :
// sideNum < 3.0 ? vec3(1.0, 1.0, 0.0) :
// sideNum < 4.0 ? vec3(0.0, 0.0, 1.0) :
// sideNum < 5.0 ? vec3(0.0, 1.0, 0.0) :
// vec3(1.0, 0.0, 1.0);
// gl_FragColor.xyz = mix(gl_FragColor.xyz, mixColor, 0.5);
// `
function createBezierMeshMaterial(baseMaterial) {
return createDerivedMaterial(
baseMaterial,
{
chained: true,
uniforms: {
pointA: {value: new Vector3()},
controlA: {value: new Vector3()},
controlB: {value: new Vector3()},
pointB: {value: new Vector3()},
radius: {value: 0.01},
dashing: {value: new Vector3()} //on, off, offset
},
vertexDefs,
vertexTransform,
fragmentDefs,
fragmentMainIntro
}
)
}
let geometry = null;
const defaultBaseMaterial = /*#__PURE__*/new MeshStandardMaterial({color: 0xffffff, side: DoubleSide});
/**
* A ThreeJS `Mesh` that bends a tube shape along a 3D cubic bezier path. The bending is done
* by deforming a straight cylindrical geometry in the vertex shader based on a set of four
* control point uniforms. It patches the necessary GLSL into the mesh's assigned `material`
* automatically.
*
* The cubiz bezier path is determined by its four `Vector3` properties:
* - `pointA`
* - `controlA`
* - `controlB`
* - `pointB`
*
* The tube's radius is controlled by its `radius` property, which defaults to `0.01`.
*
* You can also give the tube a dashed appearance with two properties:
*
* - `dashArray` - an array of two numbers, defining the length of "on" and "off" parts of
* the dash. Each is a 0-1 ratio of the entire path's length. (Actually this is the `t` length
* used as input to the cubic bezier function, not its visible length.)
* - `dashOffset` - offset of where the dash starts. You can animate this to make the dashes move.
*
* Note that the dashes will appear like a hollow tube, not solid. This will be more apparent on
* thicker tubes.
*
* TODO: proper geometry bounding sphere and raycasting
* TODO: allow control of the geometry's segment counts
*/
class BezierMesh extends Mesh {
static getGeometry() {
return geometry || (geometry =
new CylinderBufferGeometry(1, 1, 1, 6, 64).translate(0, 0.5, 0)
)
}
constructor() {
super(
BezierMesh.getGeometry(),
defaultBaseMaterial
);
this.pointA = new Vector3();
this.controlA = new Vector3();
this.controlB = new Vector3();
this.pointB = new Vector3();
this.radius = 0.01;
this.dashArray = new Vector2();
this.dashOffset = 0;
// TODO - disabling frustum culling until I figure out how to customize the
// geometry's bounding sphere that gets used
this.frustumCulled = false;
}
// Handler for automatically wrapping the base material with our upgrades. We do the wrapping
// lazily on _read_ rather than write to avoid unnecessary wrapping on transient values.
get material() {
let derivedMaterial = this._derivedMaterial;
const baseMaterial = this._baseMaterial || this._defaultMaterial || (this._defaultMaterial = defaultBaseMaterial.clone());
if (!derivedMaterial || derivedMaterial.baseMaterial !== baseMaterial) {
derivedMaterial = this._derivedMaterial = createBezierMeshMaterial(baseMaterial);
// dispose the derived material when its base material is disposed:
baseMaterial.addEventListener('dispose', function onDispose() {
baseMaterial.removeEventListener('dispose', onDispose);
derivedMaterial.dispose();
});
}
return derivedMaterial
}
set material(baseMaterial) {
this._baseMaterial = baseMaterial;
}
// Create and update material for shadows upon request:
get customDepthMaterial() {
return this.material.getDepthMaterial()
}
get customDistanceMaterial() {
return this.material.getDistanceMaterial()
}
onBeforeRender(shaderInfo) {
const {uniforms} = this.material;
const {pointA, controlA, controlB, pointB, radius, dashArray, dashOffset} = this;
uniforms.pointA.value.copy(pointA);
uniforms.controlA.value.copy(controlA);
uniforms.controlB.value.copy(controlB);
uniforms.pointB.value.copy(pointB);
uniforms.radius.value = radius;
uniforms.dashing.value.set(dashArray.x, dashArray.y, dashOffset || 0);
}
raycast(raycaster, intersects) {
// TODO - just fail for now
}
}
export { BezierMesh, createDerivedMaterial, expandShaderIncludes, getShaderUniformTypes, getShadersForMaterial, invertMatrix4, voidMainRegExp };
@@ -0,0 +1,602 @@
/**
* Lightweight thenable implementation that is entirely self-contained within a single
* function with no external dependencies so it can be easily shipped across to a WorkerModule.
*
* This implementation conforms fully to the Promises/A+ spec so it can safely interoperate
* with other thenable implementations. https://github.com/promises-aplus/promises-spec
*
* *However*, it is _not_ a full implementation of ES2015 Promises, e.g. it does not
* have the same constructor signature and does not expose a `catch` method or the static
* `resolve`/`reject`/`all`/`race` initializer methods. If you need to hand a Thenable
* instance off to consuming code that may expect a true Promise, you'll want to wrap it
* in a native-or-polyfilled Promise first.
*
* (Why yet another Promises/A+ implementation? Great question. We needed a polyfill-like
* thing that was (a) wrapped in a single function for easy serialization across to a Worker,
* and (b) was as small as possible -- at ~900B minified (~500B gzipped) this is the smallest
* implementation I've found. And also, exercises like this are challenging and fun.)
*/
function BespokeThenable() {
var state = 0; // 0=pending, 1=fulfilled, -1=rejected
var queue = [];
var value;
var scheduled = 0;
var completeCalled = 0;
function then(onResolve, onReject) {
var nextThenable = BespokeThenable();
function handleNext() {
var cb = state > 0 ? onResolve : onReject;
if (isFn(cb)) {
try {
var result = cb(value);
if (result === nextThenable) {
recursiveError();
}
var resultThen = getThenableThen(result);
if (resultThen) {
resultThen.call(result, nextThenable.resolve, nextThenable.reject);
} else {
nextThenable.resolve(result);
}
} catch (err) {
nextThenable.reject(err);
}
} else {
nextThenable[state > 0 ? 'resolve' : 'reject'](value);
}
}
queue.push(handleNext);
if (state) {
scheduleQueueFlush();
}
return nextThenable
}
var resolve = oneTime(function (val) {
if (!completeCalled) {
complete(1, val);
}
});
var reject = oneTime(function (reason) {
if (!completeCalled) {
complete(-1, reason);
}
});
function complete(st, val) {
completeCalled++;
var ignoreThrow = 0;
try {
if (val === thenableObj) {
recursiveError();
}
var valThen = st > 0 && getThenableThen(val);
if (valThen) {
valThen.call(val, oneTime(function (v) {
ignoreThrow++;
complete(1, v);
}), oneTime(function (v) {
ignoreThrow++;
complete(-1, v);
}));
} else {
state = st;
value = val;
scheduleQueueFlush();
}
} catch(e) {
if (!state && !ignoreThrow) {
complete(-1, e);
}
}
}
function scheduleQueueFlush() {
if (!scheduled) {
setTimeout(flushQueue, 0); //TODO setImmediate or postMessage approach if available?
scheduled = 1;
}
}
function flushQueue() {
var q = queue;
scheduled = 0;
queue = [];
q.forEach(callIt);
}
function callIt(fn) {
fn();
}
function getThenableThen(val) {
var valThen = val && (isFn(val) || typeof val === 'object') && val.then;
return isFn(valThen) && valThen
}
function oneTime(fn) {
var called = 0;
return function() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
if (!called++) {
fn.apply(this, args);
}
}
}
function recursiveError() {
throw new TypeError('Chaining cycle detected')
}
var isFn = function (v) { return typeof v === 'function'; };
var thenableObj = {
then: then,
resolve: resolve,
reject: reject
};
return thenableObj
}
/**
* Thenable implementation that uses a native Promise under the covers. This implementation
* is preferred if Promise is available, for better performance and dev tools integration.
* @constructor
*/
function NativePromiseThenable() {
var resolve, reject;
var promise = new Promise(function (res, rej) {
resolve = res;
reject = rej;
});
return {
then: promise.then.bind(promise),
resolve: resolve,
reject: reject
}
}
/**
* Promise.all() impl:
*/
BespokeThenable.all = NativePromiseThenable.all = function(items) {
var resultCount = 0;
var results = [];
var out = DefaultThenable();
if (items.length === 0) {
out.resolve([]);
} else {
items.forEach(function (item, i) {
var itemThenable = DefaultThenable();
itemThenable.resolve(item);
itemThenable.then(function (res) {
resultCount++;
results[i] = res;
if (resultCount === items.length) {
out.resolve(results);
}
}, out.reject);
});
}
return out
};
/**
* Choose the best Thenable implementation and export it as the default.
*/
var DefaultThenable = typeof Promise === 'function' ? NativePromiseThenable : BespokeThenable;
/**
* Main content for the worker that handles the loading and execution of
* modules within it.
*/
function workerBootstrap() {
var modules = Object.create(null);
// Handle messages for registering a module
function registerModule(ref, callback) {
var id = ref.id;
var name = ref.name;
var dependencies = ref.dependencies; if ( dependencies === void 0 ) dependencies = [];
var init = ref.init; if ( init === void 0 ) init = function(){};
var getTransferables = ref.getTransferables; if ( getTransferables === void 0 ) getTransferables = null;
// Only register once
if (modules[id]) { return }
try {
// If any dependencies are modules, ensure they're registered and grab their value
dependencies = dependencies.map(function (dep) {
if (dep && dep.isWorkerModule) {
registerModule(dep, function (depResult) {
if (depResult instanceof Error) { throw depResult }
});
dep = modules[dep.id].value;
}
return dep
});
// Rehydrate functions
init = rehydrate(("<" + name + ">.init"), init);
if (getTransferables) {
getTransferables = rehydrate(("<" + name + ">.getTransferables"), getTransferables);
}
// Initialize the module and store its value
var value = null;
if (typeof init === 'function') {
value = init.apply(void 0, dependencies);
} else {
console.error('worker module init function failed to rehydrate');
}
modules[id] = {
id: id,
value: value,
getTransferables: getTransferables
};
callback(value);
} catch(err) {
if (!(err && err.noLog)) {
console.error(err);
}
callback(err);
}
}
// Handle messages for calling a registered module's result function
function callModule(ref, callback) {
var ref$1;
var id = ref.id;
var args = ref.args;
if (!modules[id] || typeof modules[id].value !== 'function') {
callback(new Error(("Worker module " + id + ": not found or its 'init' did not return a function")));
}
try {
var result = (ref$1 = modules[id]).value.apply(ref$1, args);
if (result && typeof result.then === 'function') {
result.then(handleResult, function (rej) { return callback(rej instanceof Error ? rej : new Error('' + rej)); });
} else {
handleResult(result);
}
} catch(err) {
callback(err);
}
function handleResult(result) {
try {
var tx = modules[id].getTransferables && modules[id].getTransferables(result);
if (!tx || !Array.isArray(tx) || !tx.length) {
tx = undefined; //postMessage is very picky about not passing null or empty transferables
}
callback(result, tx);
} catch(err) {
console.error(err);
callback(err);
}
}
}
function rehydrate(name, str) {
var result = void 0;
self.troikaDefine = function (r) { return result = r; };
var url = URL.createObjectURL(
new Blob(
[("/** " + (name.replace(/\*/g, '')) + " **/\n\ntroikaDefine(\n" + str + "\n)")],
{type: 'application/javascript'}
)
);
try {
importScripts(url);
} catch(err) {
console.error(err);
}
URL.revokeObjectURL(url);
delete self.troikaDefine;
return result
}
// Handler for all messages within the worker
self.addEventListener('message', function (e) {
var ref = e.data;
var messageId = ref.messageId;
var action = ref.action;
var data = ref.data;
try {
// Module registration
if (action === 'registerModule') {
registerModule(data, function (result) {
if (result instanceof Error) {
postMessage({
messageId: messageId,
success: false,
error: result.message
});
} else {
postMessage({
messageId: messageId,
success: true,
result: {isCallable: typeof result === 'function'}
});
}
});
}
// Invocation
if (action === 'callModule') {
callModule(data, function (result, transferables) {
if (result instanceof Error) {
postMessage({
messageId: messageId,
success: false,
error: result.message
});
} else {
postMessage({
messageId: messageId,
success: true,
result: result
}, transferables || undefined);
}
});
}
} catch(err) {
postMessage({
messageId: messageId,
success: false,
error: err.stack
});
}
});
}
/**
* Fallback for `defineWorkerModule` that behaves identically but runs in the main
* thread, for when the execution environment doesn't support web workers or they
* are disallowed due to e.g. CSP security restrictions.
*/
function defineMainThreadModule(options) {
var moduleFunc = function() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return moduleFunc._getInitResult().then(function (initResult) {
if (typeof initResult === 'function') {
return initResult.apply(void 0, args)
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
};
moduleFunc._getInitResult = function() {
// We can ignore getTransferables in main thread. TODO workerId?
var dependencies = options.dependencies;
var init = options.init;
// Resolve dependencies
dependencies = Array.isArray(dependencies) ? dependencies.map(function (dep) { return dep && dep._getInitResult ? dep._getInitResult() : dep; }
) : [];
// Invoke init with the resolved dependencies
var initThenable = DefaultThenable.all(dependencies).then(function (deps) {
return init.apply(null, deps)
});
// Cache the resolved promise for subsequent calls
moduleFunc._getInitResult = function () { return initThenable; };
return initThenable
};
return moduleFunc
}
var supportsWorkers = function () {
var supported = false;
// Only attempt worker initialization in browsers; elsewhere it would just be
// noise e.g. loading into a Node environment for SSR.
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
try {
// TODO additional checks for things like importScripts within the worker?
// Would need to be an async check.
var worker = new Worker(
URL.createObjectURL(new Blob([''], { type: 'application/javascript' }))
);
worker.terminate();
supported = true;
} catch (err) {
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') ; else {
console.log(
("Troika createWorkerModule: web workers not allowed; falling back to main thread execution. Cause: [" + (err.message) + "]")
);
}
}
}
// Cached result
supportsWorkers = function () { return supported; };
return supported
};
var _workerModuleId = 0;
var _messageId = 0;
var _allowInitAsString = false;
var workers = Object.create(null);
var openRequests = /*#__PURE__*/(function () {
var obj = Object.create(null);
obj._count = 0;
return obj
})();
/**
* Define a module of code that will be executed with a web worker. This provides a simple
* interface for moving chunks of logic off the main thread, and managing their dependencies
* among one another.
*
* @param {object} options
* @param {function} options.init
* @param {array} [options.dependencies]
* @param {function} [options.getTransferables]
* @param {string} [options.name]
* @param {string} [options.workerId]
* @return {function(...[*]): {then}}
*/
function defineWorkerModule(options) {
if ((!options || typeof options.init !== 'function') && !_allowInitAsString) {
throw new Error('requires `options.init` function')
}
var dependencies = options.dependencies;
var init = options.init;
var getTransferables = options.getTransferables;
var workerId = options.workerId;
if (!supportsWorkers()) {
return defineMainThreadModule(options)
}
if (workerId == null) {
workerId = '#default';
}
var id = "workerModule" + (++_workerModuleId);
var name = options.name || id;
var registrationThenable = null;
dependencies = dependencies && dependencies.map(function (dep) {
// Wrap raw functions as worker modules with no dependencies
if (typeof dep === 'function' && !dep.workerModuleData) {
_allowInitAsString = true;
dep = defineWorkerModule({
workerId: workerId,
name: ("<" + name + "> function dependency: " + (dep.name)),
init: ("function(){return (\n" + (stringifyFunction(dep)) + "\n)}")
});
_allowInitAsString = false;
}
// Grab postable data for worker modules
if (dep && dep.workerModuleData) {
dep = dep.workerModuleData;
}
return dep
});
function moduleFunc() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
// Register this module if needed
if (!registrationThenable) {
registrationThenable = callWorker(workerId,'registerModule', moduleFunc.workerModuleData);
}
// Invoke the module, returning a thenable
return registrationThenable.then(function (ref) {
var isCallable = ref.isCallable;
if (isCallable) {
return callWorker(workerId,'callModule', {id: id, args: args})
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
}
moduleFunc.workerModuleData = {
isWorkerModule: true,
id: id,
name: name,
dependencies: dependencies,
init: stringifyFunction(init),
getTransferables: getTransferables && stringifyFunction(getTransferables)
};
return moduleFunc
}
/**
* Stringifies a function into a form that can be deserialized in the worker
* @param fn
*/
function stringifyFunction(fn) {
var str = fn.toString();
// If it was defined in object method/property format, it needs to be modified
if (!/^function/.test(str) && /^\w+\s*\(/.test(str)) {
str = 'function ' + str;
}
return str
}
function getWorker(workerId) {
var worker = workers[workerId];
if (!worker) {
// Bootstrap the worker's content
var bootstrap = stringifyFunction(workerBootstrap);
// Create the worker from the bootstrap function content
worker = workers[workerId] = new Worker(
URL.createObjectURL(
new Blob(
[("/** Worker Module Bootstrap: " + (workerId.replace(/\*/g, '')) + " **/\n\n;(" + bootstrap + ")()")],
{type: 'application/javascript'}
)
)
);
// Single handler for response messages from the worker
worker.onmessage = function (e) {
var response = e.data;
var msgId = response.messageId;
var callback = openRequests[msgId];
if (!callback) {
throw new Error('WorkerModule response with empty or unknown messageId')
}
delete openRequests[msgId];
openRequests._count--;
callback(response);
};
}
return worker
}
// Issue a call to the worker with a callback to handle the response
function callWorker(workerId, action, data) {
var thenable = DefaultThenable();
var messageId = ++_messageId;
openRequests[messageId] = function (response) {
if (response.success) {
thenable.resolve(response.result);
} else {
thenable.reject(new Error(("Error in worker " + action + " call: " + (response.error))));
}
};
openRequests._count++;
if (openRequests._count > 1000) { //detect leaks
console.warn('Large number of open WorkerModule requests, some may not be returning');
}
getWorker(workerId).postMessage({
messageId: messageId,
action: action,
data: data
});
return thenable
}
/**
* Just the {@link Thenable} function wrapped as a worker module. If another worker
* module needs Thenable as a dependency, it's better to pass this module rather than
* the raw function in its `dependencies` array so it only gets registered once.
*/
var ThenableWorkerModule = /*#__PURE__*/defineWorkerModule({
name: 'Thenable',
dependencies: [DefaultThenable],
init: function(Thenable) {
return Thenable
}
});
export { DefaultThenable as Thenable, ThenableWorkerModule, defineWorkerModule, stringifyFunction };