feat(All):Initial
This commit is contained in:
@@ -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 };
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
@@ -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}};
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
@@ -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 中)(可选);
|
||||
// DXF:X 值;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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user