From f8ae485ecf12ea32747d75d730ff5a6a5ff91ac2 Mon Sep 17 00:00:00 2001 From: EL JABIRI Tarik Date: Sun, 10 Dec 2023 20:31:31 +0100 Subject: [PATCH] feat: add dimensions in svg export --- modules/gems/exportTextData.js | 7 - modules/gems/exportTextData.ts | 5 + package-lock.json | 154 +++++++++++- package.json | 5 +- web/app/cad/actions/usabilityActions.js | 10 +- web/app/sketcher/dxf.ts | 301 ++++++++++++++---------- web/app/sketcher/io.ts | 167 ++----------- 7 files changed, 363 insertions(+), 286 deletions(-) delete mode 100644 modules/gems/exportTextData.js create mode 100644 modules/gems/exportTextData.ts diff --git a/modules/gems/exportTextData.js b/modules/gems/exportTextData.js deleted file mode 100644 index 043a40ef..00000000 --- a/modules/gems/exportTextData.js +++ /dev/null @@ -1,7 +0,0 @@ - -export default function(data, fileName) { - const link = document.getElementById("downloader"); - link.href = "data:application/octet-stream;charset=utf-8;base64," + btoa(data); - link.download = fileName; - link.click(); -} diff --git a/modules/gems/exportTextData.ts b/modules/gems/exportTextData.ts new file mode 100644 index 00000000..c9520f3c --- /dev/null +++ b/modules/gems/exportTextData.ts @@ -0,0 +1,5 @@ +import { saveAs } from "file-saver"; + +export default function (data: string, fileName: string) { + saveAs(new File([data], fileName, { type: "text/plain;charset=utf-8" })); +} diff --git a/package-lock.json b/package-lock.json index 07746a4d..c370a5bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@dxfjs/parser": "^0.2.0", "@react-icons/all-files": "^4.1.0", - "@tarikjabiri/dxf": "^2.8.9", + "@tarikjabiri/dxf": "^3.0.0-alpha.11", "@tauri-apps/api": "^1.4.0", "@types/three": "^0.146.0", "browser-xml2js": "^0.4.19", @@ -19,6 +19,7 @@ "clipper-lib": "^6.4.2", "core-js": "^3.32.2", "earcut": "^2.2.4", + "file-saver": "^2.0.5", "font-awesome": "4.7.0", "immer": "^9.0.21", "jsketcher-occ-engine": "1.0.1-f505cdcb9f37685cdadb937bc3b11b9b75f9267c", @@ -53,6 +54,7 @@ "@babel/preset-stage-2": "^7.8.3", "@babel/preset-typescript": "^7.22.15", "@tauri-apps/cli": "^1.4.0", + "@types/file-saver": "^2.0.7", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^5.62.0", @@ -79,6 +81,7 @@ "less-loader": "^11.1.3", "raw-loader": "^4.0.2", "style-loader": "^3.3.3", + "ts-loader": "^9.5.1", "typescript": "^4.9.5", "url-loader": "^4.1.1", "wait-on": "^6.0.1", @@ -2451,9 +2454,9 @@ "dev": true }, "node_modules/@tarikjabiri/dxf": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@tarikjabiri/dxf/-/dxf-2.8.9.tgz", - "integrity": "sha512-ckBkEsWdlFTvqm2ARm23xX3EVouYsnNuZWS7P/oec1WCGTjzdd9jhmtQqyhh7TQSvQo4HunbDv4Vxiv07/8zsQ==", + "version": "3.0.0-alpha.11", + "resolved": "https://registry.npmjs.org/@tarikjabiri/dxf/-/dxf-3.0.0-alpha.11.tgz", + "integrity": "sha512-YSn/wi+sYZ+bFfsImmwu9vLa0QpAxnRgpwo31TPjGUC4CFP4MhsynR/iCcPHvjWrnFrFmT3vZ4oMM8en3tUbcw==", "engines": { "node": ">=16", "pnpm": ">=8" @@ -2752,6 +2755,12 @@ "@types/send": "*" } }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, "node_modules/@types/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", @@ -6641,6 +6650,11 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/file-sync-cmp": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", @@ -11860,6 +11874,138 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", diff --git a/package.json b/package.json index 5e7e2e6a..9a4a99c7 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@babel/preset-stage-2": "^7.8.3", "@babel/preset-typescript": "^7.22.15", "@tauri-apps/cli": "^1.4.0", + "@types/file-saver": "^2.0.7", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^5.62.0", @@ -70,6 +71,7 @@ "less-loader": "^11.1.3", "raw-loader": "^4.0.2", "style-loader": "^3.3.3", + "ts-loader": "^9.5.1", "typescript": "^4.9.5", "url-loader": "^4.1.1", "wait-on": "^6.0.1", @@ -80,7 +82,7 @@ "dependencies": { "@dxfjs/parser": "^0.2.0", "@react-icons/all-files": "^4.1.0", - "@tarikjabiri/dxf": "^2.8.9", + "@tarikjabiri/dxf": "^3.0.0-alpha.11", "@tauri-apps/api": "^1.4.0", "@types/three": "^0.146.0", "browser-xml2js": "^0.4.19", @@ -88,6 +90,7 @@ "clipper-lib": "^6.4.2", "core-js": "^3.32.2", "earcut": "^2.2.4", + "file-saver": "^2.0.5", "font-awesome": "4.7.0", "immer": "^9.0.21", "jsketcher-occ-engine": "1.0.1-f505cdcb9f37685cdadb937bc3b11b9b75f9267c", diff --git a/web/app/cad/actions/usabilityActions.js b/web/app/cad/actions/usabilityActions.js index 47ea7166..e8bfeffe 100644 --- a/web/app/cad/actions/usabilityActions.js +++ b/web/app/cad/actions/usabilityActions.js @@ -1,7 +1,7 @@ import Vector, {AXIS, ORIGIN} from 'math/vector'; import {RiCamera2Line} from "react-icons/ri"; import {ViewMode} from "cad/scene/viewer"; -import {GiCube, HiCube, HiOutlineCube} from "react-icons/all"; +import {GiCube, HiCube, HiOutlineCube} from "react-icons"; const NEG_X = AXIS.X.negate(); const NEG_Y = AXIS.Y.negate(); @@ -21,7 +21,7 @@ function faceAt(shells, shell, pos) { let i = shellIndex; do { i = (i + 1) % shells.length; - shell = shells[i]; + shell = shells[i]; } while(shellIndex !== i && shell.faces.length === 0); return shell.faces[0]; } else if (pos < 0) { @@ -32,7 +32,7 @@ function faceAt(shells, shell, pos) { } while(shellIndex !== i && shell.faces.length === 0); return shell.faces[shell.faces.length - 1]; } else { - return shell.faces[pos]; + return shell.faces[pos]; } } @@ -41,7 +41,7 @@ function getCurrentSelectedOrFirstFace(ctx) { if (!face) { for (const shell of ctx.services.cadRegistry.shells) { if (shell.faces.length !== 0) { - return shell.faces[0]; + return shell.faces[0]; } } } @@ -62,7 +62,7 @@ export default [ ctx.services.viewer.zoomOut(); ctx.services.viewer.requestRender(); } - }, + }, { id: 'LookAtFace', appearance: { diff --git a/web/app/sketcher/dxf.ts b/web/app/sketcher/dxf.ts index 7e48ab3c..9155d591 100644 --- a/web/app/sketcher/dxf.ts +++ b/web/app/sketcher/dxf.ts @@ -10,12 +10,12 @@ import { Parser, PointEntity, PolylineEntity, - SplineEntity -} from "@dxfjs/parser"; -import { Colors, DLine, DxfWriter, point3d, SplineArgs_t, SplineFlags, Units, vec3_t } from '@tarikjabiri/dxf'; + SplineEntity, +} from '@dxfjs/parser'; +import { dline, Writer, point, SplineFlags, Units } from '@tarikjabiri/dxf'; import { SketchFormat_V3 } from './io'; import { Arc, SketchArcSerializationData } from './shapes/arc'; -import { BezierCurve } from "./shapes/bezier-curve"; +import { BezierCurve } from './shapes/bezier-curve'; import { Circle } from './shapes/circle'; import { AngleBetweenDimension, DiameterDimension, HDimension, LinearDimension, VDimension } from './shapes/dim'; import { Ellipse } from './shapes/ellipse'; @@ -24,15 +24,16 @@ import { EndPoint, SketchPointSerializationData } from './shapes/point'; import { Segment, SketchSegmentSerializationData } from './shapes/segment'; import { SketchObject, SketchObjectSerializationData } from './shapes/sketch-object'; import { Layer } from './viewer2d'; +import { calculateAngle, linep } from '@tarikjabiri/dxf/lib/helpers'; interface IPoint { - x: number, - y: number + x: number; + y: number; } interface SketchCircleSerializationData extends SketchObjectSerializationData { c: SketchPointSerializationData; - r: number + r: number; } interface SketchEllipseSerializationData extends SketchObjectSerializationData { @@ -48,34 +49,34 @@ interface ITransform { origin: IPoint; } -const {PI, cos, sin, atan2} = Math +const { PI, cos, sin, atan2 } = Math; export function deg(angle: number): number { - return (angle * 180) / PI + return (angle * 180) / PI; } export function rad(angle: number): number { - return (angle * PI) / 180 + return (angle * PI) / 180; } function polar(origin: IPoint, angle: number, radius: number): IPoint { return { x: origin.x + radius * cos(angle), - y: origin.y + radius * sin(angle) - } + y: origin.y + radius * sin(angle), + }; } function angle(fp: IPoint, sp: IPoint) { - let angle = Math.atan2(sp.y - fp.y, sp.x - fp.x) - if(angle < 0) angle += 2 * Math.PI - return angle + let angle = Math.atan2(sp.y - fp.y, sp.x - fp.x); + if (angle < 0) angle += 2 * Math.PI; + return angle; } function translate(p: IPoint, t: IPoint): IPoint { return { x: p.x + t.x, y: p.y + t.y, - } + }; } function rotatePoint(p: IPoint, t: ITransform): IPoint { @@ -84,8 +85,8 @@ function rotatePoint(p: IPoint, t: ITransform): IPoint { const oy = p.y - t.origin.y; return { x: t.origin.x + (ox * cos(t.rotation) - oy * sin(t.rotation)), - y: t.origin.y + (ox * sin(t.rotation) + oy * cos(t.rotation)) - } + y: t.origin.y + (ox * sin(t.rotation) + oy * cos(t.rotation)), + }; } function applyTransformPoint(p: IPoint, t: ITransform) { @@ -93,24 +94,42 @@ function applyTransformPoint(p: IPoint, t: ITransform) { } export class DxfWriterAdapter { - writer: DxfWriter; + writer: Writer; + + get modelSpace() { + return this.writer.document.modelSpace; + } + + get tables() { + return this.writer.document.tables; + } + + get bbox() { + return this.writer.document.modelSpace.bbox(); + } + + get renderer() { + return this.writer.document.renderer; + } constructor() { - this.writer = new DxfWriter(); - this.writer.setUnits(Units.Millimeters); + this.writer = new Writer(); + this.writer.document.setUnits(Units.Millimeters); + + const header = this.writer.document.header; // Dimensions customization // I hard coded these values but I am not sure about them - this.writer.setVariable('$DIMTXT', { 40: 10 }); // The text height - this.writer.setVariable('$DIMASZ', { 40: 10 }); // Dimensioning arrow size + header.add('$DIMTXT').add(40, 10); // The text height + header.add('$DIMASZ').add(40, 10); // Dimensioning arrow size // Theses for preserving the look like jsketcher - this.writer.setVariable('$DIMDEC', { 70: 2 }); // Number of precision places displayed - this.writer.setVariable('$DIMTIH', { 70: 0 }); // Text inside horizontal if nonzero - this.writer.setVariable('$DIMTOH', { 70: 0 }); // Text outside horizontal if nonzero + header.add('$DIMDEC').add(70, 2); // Number of precision places displayed + header.add('$DIMTIH').add(70, 0); // Text inside horizontal if nonzero + header.add('$DIMTOH').add(70, 0); // Text outside horizontal if nonzero // Do not force text inside extensions - this.writer.setVariable('$DIMTIX', { 70: 0 }); // Force text inside extensions if nonzero - this.writer.setVariable('$DIMATFIT', { 70: 0 }); // Controls dimension text and arrow placement + header.add('$DIMTIX').add(70, 0); // Force text inside extensions if nonzero + header.add('$DIMATFIT').add(70, 0); // Controls dimension text and arrow placement // For more customization // this.writer.setVariable('$DIMEXE', { 40: 10 }); // Extension line extension @@ -121,50 +140,54 @@ export class DxfWriterAdapter { } private _point(shape: EndPoint) { - this.writer.addPoint(shape.x, shape.y, 0); + this.modelSpace.addPoint(shape); } private _segment(shape: Segment) { - this.writer.addLine(point3d(shape.a.x, shape.a.y), point3d(shape.b.x, shape.b.y)); + this.modelSpace.addLine({ + start: point(shape.a.x, shape.a.y), + end: point(shape.b.x, shape.b.y), + }); } private _arc(shape: Arc) { - this.writer.addArc( - point3d(shape.c.x, shape.c.y), - shape.r.get(), - deg(shape.getStartAngle()), - deg(shape.getEndAngle()) - ); + this.modelSpace.addArc({ + center: point(shape.c.x, shape.c.y), + radius: shape.r.get(), + startAngle: deg(shape.getStartAngle()), + endAngle: deg(shape.getEndAngle()), + }); } private _circle(shape: Circle) { - this.writer.addCircle(point3d(shape.c.x, shape.c.y), shape.r.get()); + this.modelSpace.addCircle({ + center: point(shape.c.x, shape.c.y), + radius: shape.r.get(), + }); } private _ellipse(shape: Ellipse) { const majorX = Math.cos(shape.rotation) * shape.radiusX; const majorY = Math.sin(shape.rotation) * shape.radiusX; - this.writer.addEllipse( - point3d(shape.centerX, shape.centerY), - point3d(majorX, majorY), - shape.radiusY / shape.radiusX, - 0, - 2 * Math.PI - ); + this.modelSpace.addEllipse({ + center: point(shape.centerX, shape.centerY), + endpoint: point(majorX, majorY), + ratio: shape.radiusY / shape.radiusX, + start: 0, + end: 2 * Math.PI, + }); } private _bezier(shape: BezierCurve) { - const controlPoints: vec3_t[] = [ - point3d(shape.p0.x, shape.p0.y), - point3d(shape.p1.x, shape.p1.y), - point3d(shape.p2.x, shape.p2.y), - point3d(shape.p3.x, shape.p3.y), - ]; - const splineArgs: SplineArgs_t = { - controlPoints, + this.modelSpace.addSpline({ + controls: [ + point(shape.p0.x, shape.p0.y), + point(shape.p1.x, shape.p1.y), + point(shape.p2.x, shape.p2.y), + point(shape.p3.x, shape.p3.y), + ], flags: SplineFlags.Periodic, - }; - this.writer.addSpline(splineArgs); + }); } private _label(shape: Label) { @@ -174,22 +197,39 @@ export class DxfWriterAdapter { const h = shape.textHelper.textMetrics.width / 2; const lx = m.x - h + shape.offsetX; const ly = m.y + shape.marginOffset + shape.offsetY; - this.writer.addText(point3d(lx, ly), height, shape.text); - } - - private _vdim(shape: VDimension) { - this.writer.addLinearDim(point3d(shape.a.x, shape.a.y), point3d(shape.b.x, shape.b.y), { - angle: 90, // Make it vertical - offset: -shape.offset, + this.modelSpace.addText({ + firstAlignmentPoint: point(lx, ly), + height, + value: shape.text, }); } + private _vdim(shape: VDimension) { + const dim = this.modelSpace.addLinearDim({ + start: point(shape.a.x, shape.a.y), + end: point(shape.b.x, shape.b.y), + angle: 90, + offset: shape.offset, + }); + this.renderer.addLinear(dim); + } + private _hdim(shape: HDimension) { - this.writer.addLinearDim(point3d(shape.a.x, shape.a.y), point3d(shape.b.x, shape.b.y), { offset: -shape.offset }); + const dim = this.modelSpace.addLinearDim({ + start: point(shape.a.x, shape.a.y), + end: point(shape.b.x, shape.b.y), + offset: shape.offset, + }); + this.renderer.addLinear(dim); } private _linearDim(shape: LinearDimension) { - this.writer.addAlignedDim(point3d(shape.a.x, shape.a.y), point3d(shape.b.x, shape.b.y), { offset: shape.offset }); + const aligned = this.modelSpace.addAlignedDim({ + start: point(shape.a.x, shape.a.y), + end: point(shape.b.x, shape.b.y), + offset: -shape.offset, + }); + this.renderer.addAligned(aligned); } private _ddim(shape: DiameterDimension) { @@ -197,39 +237,36 @@ export class DxfWriterAdapter { const radius = shape.obj.distanceA ? shape.obj.distanceA() : shape.obj.r.get(); const x = shape.obj.c.x + radius * Math.cos(shape.angle); const y = shape.obj.c.y + radius * Math.sin(shape.angle); - this.writer.addRadialDim(point3d(x, y), point3d(shape.obj.c.x, shape.obj.c.y)); + const dim = this.modelSpace.addRadialDim({ + first: point(x, y), + definition: point(shape.obj.c.x, shape.obj.c.y), + leaderLength: 0, + }); + this.renderer.addRadial(dim); } private _bwdim(shape: AngleBetweenDimension) { - // This is not working as expected. - const s: DLine = { - start: point3d(shape.a.a.x, shape.a.a.y), - end: point3d(shape.a.b.x, shape.a.b.y), - }; - const f: DLine = { - start: point3d(shape.b.a.x, shape.b.a.y), - end: point3d(shape.b.b.x, shape.b.b.y), - }; - const c = point3d(shape.a.a.x, shape.a.a.y); - const offset = shape.offset; - const dyf = f.end.y - c.y; - const dys = s.end.y - c.y; - const df = Math.sqrt(Math.pow(f.end.x - c.x, 2) + Math.pow(f.end.y - c.y, 2)); - const ds = Math.sqrt(Math.pow(s.end.x - c.x, 2) + Math.pow(s.end.y - c.y, 2)); - const alphaf = Math.acos(dyf / df); - const alphas = Math.acos(dys / ds); - const alpham = Math.abs(alphaf - alphas) / 2 + (alphaf > alphas ? alphas : alphaf); - const xm = c.x + offset*Math.cos(alpham) - const ym = c.y + offset*Math.sin(alpham) - this.writer.addAngularLinesDim(f, s, point3d(xm, ym)); + dline(point(shape.a.a.x, shape.a.a.y), point(shape.a.b.x, shape.a.b.y)); + const firstLine = dline(point(shape.b.a.x, shape.b.a.y), point(shape.b.b.x, shape.b.b.y)); + const secondLine = dline(point(shape.a.a.x, shape.a.a.y), point(shape.a.b.x, shape.a.b.y)); + const fline = linep(firstLine.start, firstLine.end); + const sline = linep(secondLine.start, secondLine.end); + const intersection = fline.intersect(sline); + const angle = calculateAngle(firstLine.start, firstLine.end); + const p = polar(intersection, angle, shape.offset); + const angular = this.modelSpace.addAngularLinesDim({ + firstLine, + secondLine, + positionArc: point(p.x, p.y), + }); + this.renderer.addAngularLines(angular); } export(layers: Layer[]) { layers.forEach(layer => { - // this will prevent addLayer from throwing. - if (!this.writer.layer(layer.name)) - this.writer.addLayer(layer.name, Colors.Black); - this.writer.setCurrentLayerName(layer.name); + const found = this.tables.layer.get(layer.name); + if (found == null) this.tables.addLayer(layer); + this.modelSpace.currentLayerName = layer.name; layer.objects.forEach(shape => { if (shape instanceof EndPoint) this._point(shape); @@ -249,41 +286,51 @@ export class DxfWriterAdapter { } stringify(): string { - // reset the current layer to 0, because its preserved in the dxf. - this.writer.setZeroLayerAsCurrent(); + this.modelSpace.currentLayerName = this.tables.zeroLayer.name; return this.writer.stringify(); } } export class DxfParserAdapter { - private static _seed = 0; // Used as ids for the shapes. + private _seed = 0; // Used as ids for the shapes. private _createSketchObject(type: string, data: object) { return { - id: (DxfParserAdapter._seed++).toString(), - type, role: null, stage: 0, data - } + id: (this._seed++).toString(), + type, + role: null, + stage: 0, + data, + }; } private _createSketchFormat(obj: DxfGlobalObject): SketchFormat_V3 { - DxfParserAdapter._seed = 0; const sketch: SketchFormat_V3 = { - version: 3, objects: [], dimensions: [], labels: [], - stages: [], constants: null, metadata: {} + version: 3, + objects: [], + dimensions: [], + labels: [], + stages: [], + constants: null, + metadata: {}, }; const transform: ITransform = { translation: { x: 0, y: 0 }, rotation: 0, - origin: { x: 0, y: 0 } - } + origin: { x: 0, y: 0 }, + }; - this.handleEntities(obj.entities, sketch, transform) + this.handleEntities(obj.entities, sketch, transform); obj.entities.inserts.forEach(i => this._insert(obj.blocks, i, sketch)); return sketch; } - private handleEntities(entities: Omit, sketch: SketchFormat_V3, t: ITransform) { + private handleEntities( + entities: Omit, + sketch: SketchFormat_V3, + t: ITransform + ) { entities.arcs.forEach(a => sketch.objects.push(this._arc(a, t))); entities.circles.forEach(c => sketch.objects.push(this._circle(c, t))); entities.ellipses.forEach(e => sketch.objects.push(this._ellipse(e, t))); @@ -296,18 +343,18 @@ export class DxfParserAdapter { private _insert(blocks: Block[], i: InsertEntity, sketch: SketchFormat_V3) { const block = blocks.find(block => { - return block.name === i.blockName || block.name2 === i.blockName + return block.name === i.blockName || block.name2 === i.blockName; }); - if(block) { + if (block) { const transform: ITransform = { translation: { x: block.basePointX + i.x, - y: block.basePointY + i.y + y: block.basePointY + i.y, }, rotation: rad(i.rotation ?? 0), origin: { x: i.x, y: i.y }, - } + }; this.handleEntities(block.entities, sketch, transform); block.entities.inserts.forEach(i => this._insert(blocks, i, sketch)); } @@ -315,16 +362,16 @@ export class DxfParserAdapter { private _spline(s: SplineEntity, t: ITransform) { const objects = []; - for (let i = 0; i < s.controlPoints.length;) { + for (let i = 0; i < s.controlPoints.length; ) { const p1 = s.controlPoints[i]; const p2 = s.controlPoints[++i]; const p3 = s.controlPoints[++i]; const p4 = s.controlPoints[++i]; - if(p1 && p2 && p3 && p4) { - if(p1.x === p2.x && p3.x === p4.x && p1.y === p2.y && p3.y === p4.y) { + if (p1 && p2 && p3 && p4) { + if (p1.x === p2.x && p3.x === p4.x && p1.y === p2.y && p3.y === p4.y) { const data: SketchSegmentSerializationData = { a: applyTransformPoint({ x: p1.x, y: p1.y }, t), - b: applyTransformPoint({ x: p4.x, y: p4.y }, t) + b: applyTransformPoint({ x: p4.x, y: p4.y }, t), }; objects.push(this._createSketchObject(Segment.prototype.TYPE, data)); } else { @@ -332,7 +379,7 @@ export class DxfParserAdapter { cp4: applyTransformPoint({ x: p1.x, y: p1.y }, t), cp3: applyTransformPoint({ x: p2.x, y: p2.y }, t), cp2: applyTransformPoint({ x: p3.x, y: p3.y }, t), - cp1: applyTransformPoint({ x: p4.x, y: p4.y }, t) + cp1: applyTransformPoint({ x: p4.x, y: p4.y }, t), }; objects.push(this._createSketchObject(BezierCurve.prototype.TYPE, data)); } @@ -347,28 +394,28 @@ export class DxfParserAdapter { private _lwPolyline(p: LWPolylineEntity | PolylineEntity, t: ITransform) { const objects = []; - for (let i = 0; i < p.vertices.length;) { + for (let i = 0; i < p.vertices.length; ) { const curr = p.vertices[i]; let next = p.vertices[++i]; - if(p.flag & 1 && !next) { + if (p.flag & 1 && !next) { next = p.vertices[0]; } - if(curr && next) { - if(!curr.bulge || curr.bulge === 0) { + if (curr && next) { + if (!curr.bulge || curr.bulge === 0) { const data: SketchSegmentSerializationData = { a: applyTransformPoint({ x: curr.x, y: curr.y }, t), - b: applyTransformPoint({x: next.x, y: next.y}, t) + b: applyTransformPoint({ x: next.x, y: next.y }, t), }; objects.push(this._createSketchObject(Segment.prototype.TYPE, data)); } else { const beta = angle(curr, next); const theta = 4 * Math.atan(curr.bulge); - const radius = (Math.hypot(curr.x - next.x, curr.y - next.y) / 2) / Math.sin(theta / 2); + const radius = Math.hypot(curr.x - next.x, curr.y - next.y) / 2 / Math.sin(theta / 2); const center = polar(curr, beta + (Math.PI - theta) / 2, radius); - const data: SketchArcSerializationData = { - a: applyTransformPoint(curr.bulge > 0 ? {x: curr.x, y: curr.y} : {x: next.x, y: next.y}, t), - b: applyTransformPoint(curr.bulge > 0 ? {x: next.x, y: next.y} : {x: curr.x, y: curr.y}, t), + const data: SketchArcSerializationData = { + a: applyTransformPoint(curr.bulge > 0 ? { x: curr.x, y: curr.y } : { x: next.x, y: next.y }, t), + b: applyTransformPoint(curr.bulge > 0 ? { x: next.x, y: next.y } : { x: curr.x, y: curr.y }, t), c: applyTransformPoint(center, t), }; objects.push(this._createSketchObject(Arc.prototype.TYPE, data)); @@ -379,8 +426,8 @@ export class DxfParserAdapter { } private _arc(a: ArcEntity, t: ITransform) { - const center: IPoint = {x: a.centerX, y: a.centerY}; - const data: SketchArcSerializationData = { + const center: IPoint = { x: a.centerX, y: a.centerY }; + const data: SketchArcSerializationData = { a: applyTransformPoint(polar(center, rad(a.startAngle), a.radius), t), b: applyTransformPoint(polar(center, rad(a.endAngle), a.radius), t), c: applyTransformPoint(center, t), @@ -391,13 +438,13 @@ export class DxfParserAdapter { private _circle(c: CircleEntity, t: ITransform) { const data: SketchCircleSerializationData = { c: applyTransformPoint({ x: c.centerX, y: c.centerY }, t), - r: c.radius + r: c.radius, }; return this._createSketchObject(Circle.prototype.TYPE, data); } private _ellipse(e: EllipseEntity, t: ITransform) { - const c: IPoint = applyTransformPoint({x: e.centerX, y: e.centerY}, t); + const c: IPoint = applyTransformPoint({ x: e.centerX, y: e.centerY }, t); let rot = atan2(e.majorAxisY, e.majorAxisX); const rx = e.majorAxisX / cos(rot); const ry = e.ratioOfMinorAxisToMajorAxis * rx; @@ -408,8 +455,8 @@ export class DxfParserAdapter { private _segment(l: LineEntity, t: ITransform) { const data: SketchSegmentSerializationData = { - a: applyTransformPoint({x: l.startX, y: l.startY}, t), - b: applyTransformPoint({x: l.endX, y: l.endY}, t) + a: applyTransformPoint({ x: l.startX, y: l.startY }, t), + b: applyTransformPoint({ x: l.endX, y: l.endY }, t), }; return this._createSketchObject(Segment.prototype.TYPE, data); } @@ -423,7 +470,7 @@ export class DxfParserAdapter { return new Promise((resolve, reject) => { new Parser() .parse(dxfString) - .then(dxfObject => resolve(this._createSketchFormat(dxfObject))) + .then(dxfObject => resolve(this._createSketchFormat(dxfObject))) .catch(error => reject(error)); }); } diff --git a/web/app/sketcher/io.ts b/web/app/sketcher/io.ts index f35d5b32..d0fbdbbb 100644 --- a/web/app/sketcher/io.ts +++ b/web/app/sketcher/io.ts @@ -15,19 +15,15 @@ import { LinearDimension, VDimension, } from './shapes/dim'; -import Vector from 'math/vector'; import exportTextData from 'gems/exportTextData'; -import { - AlgNumConstraint, - ConstraintSerialization, -} from './constr/ANConstraints'; +import { AlgNumConstraint, ConstraintSerialization } from './constr/ANConstraints'; import { SketchGenerator } from './generators/sketchGenerator'; import { BoundaryGeneratorSchema } from './generators/boundaryGenerator'; import { ShapesTypes } from './shapes/sketch-types'; import { SketchObject } from './shapes/sketch-object'; import { Label } from 'sketcher/shapes/label'; import { DxfWriterAdapter } from './dxf'; -import { DEG_RAD } from 'math/commons'; +import { svg } from '@tarikjabiri/dxf/lib/svg'; export interface SketchFormat_V3 { version: number; @@ -76,11 +72,11 @@ export class IO { viewer: Viewer; - constructor(viewer) { + constructor(viewer: Viewer) { this.viewer = viewer; } - loadSketch(sketchData) { + loadSketch(sketchData: string) { return this._loadSketch(JSON.parse(sketchData)); } @@ -154,12 +150,7 @@ export class IO { } else if (type === VDimension.prototype.TYPE) { skobj = LinearDimension.load(VDimension, obj.id, obj.data, index); } else if (type === LinearDimension.prototype.TYPE) { - skobj = LinearDimension.load( - LinearDimension, - obj.id, - obj.data, - index - ); + skobj = LinearDimension.load(LinearDimension, obj.id, obj.data, index); } else if (type === DiameterDimension.prototype.TYPE) { skobj = DiameterDimension.load(obj.id, obj.data, index); } else if (type === AngleBetweenDimension.prototype.TYPE) { @@ -201,9 +192,7 @@ export class IO { stage.addConstraint(constraint); } catch (e) { console.error(e); - console.error( - 'skipping errant constraint: ' + constr && constr.typeId - ); + console.error('skipping errant constraint: ' + constr && constr.typeId); } } for (const gen of dataStage.generators) { @@ -235,10 +224,7 @@ export class IO { BoundaryGeneratorSchema ); - this.viewer.parametricManager.addGeneratorToStage( - boundaryGenerator, - this.viewer.parametricManager.groundStage - ); + this.viewer.parametricManager.addGeneratorToStage(boundaryGenerator, this.viewer.parametricManager.groundStage); } cleanUpData() { @@ -333,11 +319,7 @@ export class IO { } getWorkspaceToExport() { - return [ - this.viewer.layers, - [this.viewer.labelLayer], - this.viewer.dimLayers - ]; + return [this.viewer.layers, [this.viewer.labelLayer], this.viewer.dimLayers]; } getLayersToExport() { @@ -354,107 +336,26 @@ export class IO { } svgExport() { - const T = ShapesTypes; - const out = new TextBuilder(); + const adapter = new DxfWriterAdapter(); + adapter.export(this.getLayersToExport()); - const bbox = new BBox(); + // TODO: find a better way + const bbox = adapter.bbox; + const factor = (bbox.maxX - bbox.minX) / 125; + adapter.renderer.arrowSize *= factor; + adapter.renderer.extensionOffset *= factor; + adapter.renderer.extensionOverShoot *= factor; + adapter.renderer.textHeight *= factor; - const a = new Vector(); - const b = new Vector(); + adapter.renderer.draw(); - const prettyColors = new PrettyColors(); - const toExport = this.getLayersToExport(); - for (let l = 0; l < toExport.length; ++l) { - const layer = toExport[l]; - const color = prettyColors.next(); - out.fline('', [ - layer.name, - 'none', - color, - '2', - ]); - for (let i = 0; i < layer.objects.length; ++i) { - const obj = layer.objects[i]; - if (obj.TYPE !== T.POINT) bbox.check(obj); - if (obj instanceof Segment) { - out.fline('', [ - obj.a.x, - obj.a.y, - obj.b.x, - obj.b.y, - ]); - } else if (obj instanceof Arc) { - a.set(obj.a.x - obj.c.x, obj.a.y - obj.c.y, 0); - b.set(obj.b.x - obj.c.x, obj.b.y - obj.c.y, 0); - const dir = a.cross(b).z > 0 ? 0 : 1; - const r = obj.r.get(); - out.fline('', [ - obj.a.x, - obj.a.y, - r, - r, - dir, - 1, - obj.b.x, - obj.b.y, - ]); - } else if (obj instanceof Circle) { - out.fline('', [ - obj.c.x, - obj.c.y, - obj.r.get(), - ]); - // } else if (obj.TYPE === T.DIM || obj.TYPE === T.HDIM || obj.TYPE === T.VDIM) { - } else if (obj instanceof EllipticalArc) { - a.set(obj.a.x - obj.c.x, obj.a.y - obj.c.y, 0); - b.set(obj.b.x - obj.c.x, obj.b.y - obj.c.y, 0); - const dir = a.cross(b).z > 0 ? 0 : 1; - out.fline('', [ - obj.a.x, - obj.a.y, - obj.radiusX, - obj.radiusY, - obj.rotation / DEG_RAD, - dir, - obj.b.x, - obj.b.y - ]); - } else if (obj instanceof Ellipse) { - out.fline('', [ - obj.c.x, - obj.c.y, - obj.radiusX, - obj.radiusY, - obj.rotation / DEG_RAD, - obj.c.x, - obj.c.y, - ]); - } else if (obj instanceof BezierCurve) { - out.fline('', [ - obj.a.x, - obj.a.y, - obj.cp1.x, - obj.cp1.y, - obj.cp2.x, - obj.cp2.y, - obj.b.x, - obj.b.y - ]); - } - } - out.line(''); - } - bbox.inc(20); - bbox.bbox[2] -= bbox.bbox[0]; - bbox.bbox[3] -= bbox.bbox[1]; - return ( - _format("\n", bbox.bbox) + out.data + '' - ); + return svg(adapter.writer.document); } dxfExport() { const adapter = new DxfWriterAdapter(); adapter.export(this.getLayersToExport()); + adapter.renderer.draw(); return adapter.stringify(); } } @@ -463,8 +364,7 @@ function _format(str, args) { if (args.length == 0) return str; let i = 0; return str.replace(/\$/g, function () { - if (args === undefined || args[i] === undefined) - throw 'format arguments mismatch'; + if (args === undefined || args[i] === undefined) throw 'format arguments mismatch'; let val = args[i]; if (typeof val === 'number') val = val.toPrecision(); i++; @@ -474,14 +374,7 @@ function _format(str, args) { /** @constructor */ function PrettyColors() { - const colors = [ - '#000000', - '#00008B', - '#006400', - '#8B0000', - '#FF8C00', - '#E9967A', - ]; + const colors = ['#000000', '#00008B', '#006400', '#8B0000', '#FF8C00', '#E9967A']; let colIdx = 0; this.next = function () { return colors[colIdx++ % colors.length]; @@ -508,19 +401,13 @@ function TextBuilder() { /** @constructor */ function BBox() { - const bbox = [ - Number.MAX_VALUE, - Number.MAX_VALUE, - -Number.MAX_VALUE, - -Number.MAX_VALUE, - ]; + const bbox = [Number.MAX_VALUE, Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE]; const T = ShapesTypes; this.checkLayers = function (layers) { for (let l = 0; l < layers.length; ++l) - for (let i = 0; i < layers[l].objects.length; ++i) - this.check(layers[l].objects[i]); + for (let i = 0; i < layers[l].objects.length; ++i) this.check(layers[l].objects[i]); }; this.check = function (obj) { @@ -534,11 +421,7 @@ function BBox() { } else if (obj.TYPE === T.CIRCLE) { this.checkCircBounds(obj.c.x, obj.c.y, obj.r.get()); } else if (obj.TYPE === T.ELLIPSE || obj.TYPE === T.ELL_ARC) { - this.checkCircBounds( - obj.centerX, - obj.centerY, - Math.max(obj.radiusX, obj.radiusY) - ); + this.checkCircBounds(obj.centerX, obj.centerY, Math.max(obj.radiusX, obj.radiusY)); } else if (obj) { obj.accept(o => { if (o.TYPE == T.POINT) {