From 2c02b107f5987d75693eba9b94175e0ea2aa0632 Mon Sep 17 00:00:00 2001 From: EL JABIRI Tarik Date: Sun, 26 Nov 2023 19:19:54 +0100 Subject: [PATCH] feat: add svg export (#164) --- .gitignore | 1 + examples/dimension.ts | 19 +- examples/index.ts | 1 + examples/svg.ts | 45 ++++ examples/utils.ts | 4 +- src/blocks/blocks.ts | 5 + src/document.ts | 10 +- src/entities/dimension/aligned.ts | 20 +- src/entities/dimension/index.ts | 1 + src/entities/dimension/render/arrow.ts | 49 ++++ src/entities/dimension/render/index.ts | 2 + src/entities/dimension/render/renderer.ts | 119 ++++++++++ src/entities/manager.ts | 1 + src/helpers/angles.ts | 2 +- src/helpers/primitives/arc.ts | 52 ++++- src/helpers/primitives/line.ts | 36 ++- src/helpers/primitives/vector.ts | 11 +- src/helpers/transform.ts | 19 +- src/svg/colors.ts | 258 ++++++++++++++++++++++ src/svg/elements.ts | 91 ++++++++ src/svg/exporter.ts | 67 ++++++ src/svg/guards.ts | 21 ++ src/svg/index.ts | 8 + src/tables/layer.ts | 8 +- src/tables/tables.ts | 6 + tsup.config.ts | 2 +- 26 files changed, 824 insertions(+), 34 deletions(-) create mode 100644 examples/svg.ts create mode 100644 src/entities/dimension/render/arrow.ts create mode 100644 src/entities/dimension/render/index.ts create mode 100644 src/entities/dimension/render/renderer.ts create mode 100644 src/svg/colors.ts create mode 100644 src/svg/elements.ts create mode 100644 src/svg/exporter.ts create mode 100644 src/svg/guards.ts create mode 100644 src/svg/index.ts diff --git a/.gitignore b/.gitignore index eac50267..9df609f6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ coverage **/*.dwl2 **/*.err **/*.py +**/*.svg diff --git a/examples/dimension.ts b/examples/dimension.ts index e92e6439..58ad7d37 100644 --- a/examples/dimension.ts +++ b/examples/dimension.ts @@ -13,17 +13,21 @@ const green = writer.document.tables.addLayer({ modelSpace.currentLayerName = green.name; -modelSpace.addAlignedDim({ +const aligned = modelSpace.addAlignedDim({ start: point(), end: point(100, 100), offset: 10, + dimStyleName: writer.document.tables.dimStyleStandard.options.name }); +writer.document.renderer.aligned(aligned); + modelSpace.addLinearDim({ start: point(), end: point(100, 100), offset: 10, angle: 0, + dimStyleName: writer.document.tables.dimStyleStandard.options.name }); writer.document.tables.dimStyleStandard.options.DIMTXT = 2.5; @@ -36,14 +40,17 @@ const dim = modelSpace.addAngularLinesDim({ measurement: 0.785398, dimStyleName: writer.document.tables.dimStyleStandard.options.name, }); - +writer.document.renderer.angularLines(dim); writer.document.tables.dimStyleStandard.reactors.add(330, dim.handle); modelSpace.addAngularPointsDim({ - center: point(100), - first: point(100, 100), - second: point(200), - middle: point(134.9199, 93.7049), + center: point(), + first: point(20, 10), + second: point(10, 20), + definition: point(14, 14), + middle: point(15, 15), + measurement: 0.6435011087932843, + dimStyleName: writer.document.tables.dimStyleStandard.options.name }); modelSpace.addRadialDim({ diff --git a/examples/index.ts b/examples/index.ts index 59429b97..3a44683c 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -11,5 +11,6 @@ import "./paper-space"; import "./polyline"; import "./quick-start"; import "./rectangle"; +import "./svg"; import "./table"; import "./text"; diff --git a/examples/svg.ts b/examples/svg.ts new file mode 100644 index 00000000..c2071a2a --- /dev/null +++ b/examples/svg.ts @@ -0,0 +1,45 @@ +import { Colors, Writer, dline, point } from "@/index"; +import { fileURLToPath, save } from "./utils"; +import { svg } from "@/svg"; + +const writer = new Writer(); +const modelSpace = writer.document.modelSpace; + +const green = writer.document.tables.addLayer({ + name: "Green", + colorNumber: Colors.Green, +}); + +const blue = writer.document.tables.addLayer({ + name: "Blue", + colorNumber: Colors.Blue, +}); + +modelSpace.currentLayerName = green.name; + +modelSpace.addLine({ start: point(), end: point(20, 20) }); +modelSpace.addLine({ start: point(0, 2.5), end: point(20, 2.5) }); +modelSpace.addLine({ start: point(0, 5), end: point(5, 10) }); + +modelSpace.currentLayerName = blue.name; + +const aligned = modelSpace.addAlignedDim({ + start: point(), + end: point(20, 20), + offset: 5, +}); + +writer.document.renderer.aligned(aligned); + + +const angular = modelSpace.addAngularLinesDim({ + firstLine: dline(point(0, 2.5), point(20, 2.5)), + secondLine: dline(point(0, 5), point(5, 10)), + positionArc: point(15, 10), + middle: point(6.575676, 6.259268), + measurement: 0.785398, +}); + +writer.document.renderer.angularLines(angular); + +save(svg(writer.document), fileURLToPath(import.meta.url), ".svg"); diff --git a/examples/utils.ts b/examples/utils.ts index 2b7ef35b..c4211b71 100644 --- a/examples/utils.ts +++ b/examples/utils.ts @@ -3,8 +3,8 @@ import { basename } from "path"; export { fileURLToPath } from "url"; -export function save(content: string, filename: string) { +export function save(content: string, filename: string, ext = ".dxf") { mkdirSync("output", { recursive: true }); - const _name = `output/${basename(filename).replace(".ts", ".dxf")}`; + const _name = `output/${basename(filename).replace(".ts", ext)}`; writeFileSync(_name, content); } diff --git a/src/blocks/blocks.ts b/src/blocks/blocks.ts index 619f55f1..c23669d5 100644 --- a/src/blocks/blocks.ts +++ b/src/blocks/blocks.ts @@ -34,6 +34,11 @@ export class Blocks implements Taggable, WithSeeder { return b; } + get(name?: string) { + if (name == null) return; + return this.blocks.find(b => b.name === name); + } + addPaperSpace() { const name = `*Paper_Space${this.paperSpaceSeed++}`; return this.addBlock({ name }); diff --git a/src/document.ts b/src/document.ts index ca78418f..be8e5635 100644 --- a/src/document.ts +++ b/src/document.ts @@ -1,7 +1,13 @@ import { BBox, Seeder, TagsManager, Units, point2d } from "./utils"; import { BlockOptions, Blocks } from "./blocks"; -import { OmitBlockRecord, OmitSeeder, Stringifiable, WithSeeder } from "./types"; +import { + OmitBlockRecord, + OmitSeeder, + Stringifiable, + WithSeeder, +} from "./types"; import { Classes } from "./classes"; +import { DimensionRenderer } from "./entities"; import { Entities } from "./entities"; import { Header } from "./header"; import { Objects } from "./objects"; @@ -15,6 +21,7 @@ export class Document implements Stringifiable, WithSeeder { readonly entities: Entities; readonly tables: Tables; readonly objects: Objects; + readonly renderer: DimensionRenderer; units: number; @@ -34,6 +41,7 @@ export class Document implements Stringifiable, WithSeeder { this.blocks = new Blocks(this); this.entities = new Entities(this); this.objects = new Objects(this); + this.renderer = new DimensionRenderer(this); this.units = Units.Unitless; } diff --git a/src/entities/dimension/aligned.ts b/src/entities/dimension/aligned.ts index 081be5f6..1189f03a 100644 --- a/src/entities/dimension/aligned.ts +++ b/src/entities/dimension/aligned.ts @@ -1,6 +1,7 @@ import { Dimension, DimensionOptions, DimensionType } from "./dimension"; import { TagsManager, angle, point, polar } from "@/utils"; import { Point3D } from "@/types"; +import { linep } from "@/helpers"; export interface AlignedDimensionOptions extends DimensionOptions { insertion?: Point3D; @@ -14,13 +15,16 @@ export class AlignedDimension extends Dimension { start: Point3D; end: Point3D; + readonly offset: number; + constructor(options: AlignedDimensionOptions) { super(options); this.dimensionType = DimensionType.Aligned; this.insertion = options.insertion; this.start = options.start; this.end = options.end; - this.offset(options.offset); + this.offset = options.offset ?? 0; + this._offset(); } protected override tagifyChild(mg: TagsManager): void { @@ -31,10 +35,14 @@ export class AlignedDimension extends Dimension { mg.point(this.end, 4); } - private offset(v?: number) { - if (v == null) return; - const { start, end } = this; - const middle = point((start.x + end.x) / 2, (start.y + end.y) / 2); - this.definition = polar(middle, angle(this.start, this.end) - 90, v); + private _offset() { + const { offset } = this; + if (offset == null) return; + const sign = Math.sign(this.offset); + const a = angle(this.start, this.end) + 90 * sign; + const start = polar(this.start, a, this.offset); + this.definition = polar(this.end, a, this.offset); + const middle = linep(start, this.definition).middle; + this.middle = point(middle.x, middle.y); } } diff --git a/src/entities/dimension/index.ts b/src/entities/dimension/index.ts index 13be02fa..12e439a2 100644 --- a/src/entities/dimension/index.ts +++ b/src/entities/dimension/index.ts @@ -5,3 +5,4 @@ export * from "./diameter"; export * from "./dimension"; export * from "./linear"; export * from "./radial"; +export * from "./render"; diff --git a/src/entities/dimension/render/arrow.ts b/src/entities/dimension/render/arrow.ts new file mode 100644 index 00000000..1d27a4b5 --- /dev/null +++ b/src/entities/dimension/render/arrow.ts @@ -0,0 +1,49 @@ +import { Point2D, Point3D, WithSeeder } from "@/types"; +import { Seeder, point, point2d } from "@/utils"; +import { Solid } from "@/entities"; +import { transform } from "@/helpers"; + +export interface DimensionArrowOptions extends WithSeeder { + size?: number; + rotation?: number; + position?: Point3D; +} + +export function arrow(options: DimensionArrowOptions) { + return new DimensionArrow(options).entity(); +} + +export class DimensionArrow { + readonly seeder: Seeder; + size: number; + rotation: number; + position: Point3D; + + constructor(options: DimensionArrowOptions) { + this.seeder = options.seeder; + this.size = options.size ?? 2.5; + this.rotation = options.rotation ?? 0; + this.position = options.position ?? point(); + } + + entity() { + const { size: s, seeder } = this; + const h = this.size / 3 / 2; + return new Solid({ + seeder, + first: this.position, + second: this._transform(point2d(-s, -h)), + third: this._transform(point2d(-s, h)), + }); + } + + private _transform(target: Point2D) { + const result = transform({ + target, + center: this.position, + angle: this.rotation, + translation: this.position, + }); + return point(result.x, result.y); + } +} diff --git a/src/entities/dimension/render/index.ts b/src/entities/dimension/render/index.ts new file mode 100644 index 00000000..17fd8013 --- /dev/null +++ b/src/entities/dimension/render/index.ts @@ -0,0 +1,2 @@ +export * from "./arrow"; +export * from "./renderer"; diff --git a/src/entities/dimension/render/renderer.ts b/src/entities/dimension/render/renderer.ts new file mode 100644 index 00000000..a5579093 --- /dev/null +++ b/src/entities/dimension/render/renderer.ts @@ -0,0 +1,119 @@ +import { AlignedDimension, AngularLinesDimension, AttachmentPoint, arrow } from "@/entities"; +import { ArcPrimitive, LinePrimitive, PI, calculateAngle, linep } from "@/helpers"; +import { Block, Blocks } from "@/blocks"; +import { Seeder, angle, deg, point, polar } from "@/utils"; +import { Tables } from "@/tables"; +import { WithSeeder } from "@/types"; + +function round(v: number, accuracy = 0.01) { + const EPSILON = Number.EPSILON || Math.pow(2, -52); + const temp = 1 / accuracy; + return Math.round((v + EPSILON) * temp) / temp; +} + +export interface DimensionRendererOptions extends WithSeeder { + blocks: Blocks; + tables: Tables; +} + +export class DimensionRenderer implements WithSeeder { + readonly seeder: Seeder; + readonly blocks: Blocks; + readonly tables: Tables; + + private _seed: number; + private get _blockName() { + return `*D${++this._seed}`; + } + + constructor(options: DimensionRendererOptions) { + this.seeder = options.seeder; + this.blocks = options.blocks; + this.tables = options.tables; + this._seed = 0; + } + + aligned(dim: AlignedDimension) { + const { seeder } = this.blocks; + const rotation = calculateAngle(dim.start, dim.end); + const sign = Math.sign(dim.offset); + const block = this.blocks.addBlock({ name: this._blockName }); + block.currentLayerName = dim.layerName || "0"; + const angle = rotation + (PI / 2) * sign; + const start = polar(dim.start, deg(angle), dim.offset); + const end = polar(dim.end, deg(angle), dim.offset); + block.push(arrow({ seeder, rotation: rotation - PI, position: start })); + block.push(arrow({ seeder, rotation, position: end })); + linep(start, end).trimStart(2.5).trimEnd(2.5).write(block); + this._trimExpand(linep(dim.start, start), block); + this._trimExpand(linep(dim.end, end), block); + const layerName = this.tables.addDefpointsLayer(); + block.addPoint({ ...dim.start, layerName }); + block.addPoint({ ...dim.end, layerName }); + + const distance = round(linep(dim.start, dim.end).length); + const middle = linep(start, end).middle; + + dim.textRotation = deg(rotation); + dim.measurement = distance; + dim.middle = polar(middle, 90 * sign, 1.25); + block.addMText({ + insertionPoint: dim.middle, + height: 2.5, + value: distance.toString(), + rotation: deg(rotation), + attachmentPoint: AttachmentPoint.BottomCenter, + }); + + dim.blockName = block.name; + } + + angularLines(dim: AngularLinesDimension) { + const { seeder } = this.blocks; + const block = this.blocks.addBlock({ name: this._blockName }); + block.currentLayerName = dim.layerName || "0"; + const fline = linep(dim.firstLine.start, dim.firstLine.end); + const sline = linep(dim.secondLine.start, dim.secondLine.end); + const intersection = fline.intersect(sline); + if (intersection == null) return; + const center = point(intersection.x, intersection.y); + const radius = linep(center, dim.positionArc).length; + const startAngle = angle(dim.firstLine.start, dim.firstLine.end); + const endAngle = angle(dim.secondLine.start, dim.secondLine.end); + const arc = new ArcPrimitive({ center, endAngle, startAngle, radius }); + const tarc = arc.trimStart(2.5, true).trimEnd(2.5, true); + tarc.write(block); + block.push(arrow({ + seeder, + rotation: calculateAngle(tarc.start, arc.start), + position: arc.start + })); + block.push(arrow({ + seeder, + rotation: calculateAngle(tarc.end, arc.end), + position: arc.end + })); + dim.middle = polar(arc.middle, arc.middleAngle, 1.25); + block.addMText({ + insertionPoint: dim.middle, + height: 2.5, + value: `${arc.angle.toFixed(2)}°`, + rotation: arc.middleAngle - 90, + attachmentPoint: AttachmentPoint.BottomCenter, + }); + + if (fline.length <= radius) { + this._trimExpand(linep(fline.end, arc.start), block); + } + + if (sline.length <= radius) { + this._trimExpand(linep(sline.end, arc.end), block); + } + + dim.blockName = block.name; + } + + private _trimExpand(line: LinePrimitive, block: Block) { + line.trimStart(0.625).expandEnd(1.25).write(block); + } +} diff --git a/src/entities/manager.ts b/src/entities/manager.ts index 367f9110..ad39237f 100644 --- a/src/entities/manager.ts +++ b/src/entities/manager.ts @@ -81,6 +81,7 @@ export class EntitiesManager implements Taggable, WithSeeder { push(entity?: Entity) { if (entity == null) return; + if (entity.layerName == null) entity.layerName = this.currentLayerName; this.entities.push(entity); } diff --git a/src/helpers/angles.ts b/src/helpers/angles.ts index 7a1f328b..c7783664 100644 --- a/src/helpers/angles.ts +++ b/src/helpers/angles.ts @@ -1,5 +1,5 @@ import { TOW_PI, periodic } from "@/helpers"; -import { Point2D } from "@/index"; +import { Point2D } from "@/types"; export function pdeg(value: number) { return periodic(value, 0, 360); diff --git a/src/helpers/primitives/arc.ts b/src/helpers/primitives/arc.ts index ab148580..0f3110be 100644 --- a/src/helpers/primitives/arc.ts +++ b/src/helpers/primitives/arc.ts @@ -1,4 +1,3 @@ -import { Block, Point2D, deg, point } from "@/index"; import { HALF_PI, LinePrimitive, @@ -6,7 +5,11 @@ import { Writable, angleBetween, calculateAngle, + periodic, } from "@/helpers"; +import { deg, point, polar } from "@/utils"; +import { Block } from "@/blocks"; +import { Point2D } from "@/types"; export interface ArcPrimitiveOptions { center: Point2D; @@ -23,6 +26,17 @@ export class ArcPrimitive implements Writable { endAngle: number; clockwise: boolean; + get angle() { + const { startAngle, endAngle } = this.ccw; + return periodic(endAngle - startAngle, 0, 360); + } + + get middleAngle() { + const { startAngle, endAngle } = this.ccw; + if (this.clockwise) return endAngle + this.angle / 2; + return startAngle + this.angle / 2; + } + get cw() { if (this.clockwise) return this.clone(); else { @@ -50,6 +64,18 @@ export class ArcPrimitive implements Writable { } else return this.clone(); } + get start() { + return polar(this.center, this.startAngle, this.radius); + } + + get middle() { + return polar(this.center, this.middleAngle, this.radius); + } + + get end() { + return polar(this.center, this.endAngle, this.radius); + } + static from3Points(start: Point2D, middle: Point2D, end: Point2D) { const line1 = new LinePrimitive(start, middle); const line2 = new LinePrimitive(middle, end); @@ -88,6 +114,30 @@ export class ArcPrimitive implements Writable { this.clockwise = options.clockwise || false; } + trimStart(length: number, isChord?: boolean) { + const a = isChord + ? 2 * Math.asin(length / (2 * this.radius)) + : length / this.radius; + + const clone = this.ccw; + clone.startAngle += deg(a); + + if (this.clockwise) return clone.cw; + return clone; + } + + trimEnd(length: number, isChord?: boolean) { + const a = isChord + ? 2 * Math.asin(length / (2 * this.radius)) + : length / this.radius; + + const clone = this.ccw; + clone.endAngle -= deg(a); + + if (this.clockwise) return clone.cw; + return clone; + } + clone() { const { center, radius, startAngle, endAngle, clockwise } = this; return new ArcPrimitive({ diff --git a/src/helpers/primitives/line.ts b/src/helpers/primitives/line.ts index 73f5da5b..07c44e38 100644 --- a/src/helpers/primitives/line.ts +++ b/src/helpers/primitives/line.ts @@ -1,9 +1,15 @@ -import { Block, Point2D, point } from "@/index"; import { Vector, Writable, rotate } from "@/helpers"; +import { Block } from "@/blocks"; +import { Point2D } from "@/types"; +import { point } from "@/utils"; + +export function linep(start: Point2D, end: Point2D) { + return new LinePrimitive(start, end); +} export class LinePrimitive implements Writable { - start: Vector; - end: Vector; + readonly start: Vector; + readonly end: Vector; get vector(): Vector { return Vector.from(this.start, this.end); @@ -13,6 +19,10 @@ export class LinePrimitive implements Writable { return this.start.add(this.end).scale(0.5); } + get length(): number { + return this.vector.length(); + } + constructor(start: Point2D, end: Point2D) { this.start = new Vector(start.x, start.y); this.end = new Vector(end.x, end.y); @@ -37,6 +47,26 @@ export class LinePrimitive implements Writable { return this.start.add(fv.scale(t)); } + trimStart(offset: number) { + const vector = this.vector.normalize().scale(offset); + return new LinePrimitive(this.start.add(vector), this.end); + } + + trimEnd(offset: number) { + const vector = this.vector.normalize().scale(-offset); + return new LinePrimitive(this.start, this.end.add(vector)); + } + + expandStart(offset: number) { + const vector = this.vector.normalize().scale(-offset); + return new LinePrimitive(this.start.add(vector), this.end); + } + + expandEnd(offset: number) { + const vector = this.vector.normalize().scale(offset); + return new LinePrimitive(this.start, this.end.add(vector)); + } + write(block: B) { const start = point(this.start.x, this.start.y); const end = point(this.end.x, this.end.y); diff --git a/src/helpers/primitives/vector.ts b/src/helpers/primitives/vector.ts index 90efefeb..f4b93dbd 100644 --- a/src/helpers/primitives/vector.ts +++ b/src/helpers/primitives/vector.ts @@ -1,4 +1,4 @@ -import { Point2D } from "@/index"; +import { Point2D } from "@/types"; export class Vector implements Point2D { x: number; @@ -17,6 +17,15 @@ export class Vector implements Point2D { return new Vector(this.x + rhs.x, this.y + rhs.y); } + length() { + return Math.hypot(this.x, this.y); + } + + normalize() { + const length = this.length(); + return new Vector(this.x / length, this.y / length); + } + distance(rhs: Vector) { return Math.hypot(rhs.x - this.x, rhs.y - this.y); } diff --git a/src/helpers/transform.ts b/src/helpers/transform.ts index ac9c965d..212eef31 100644 --- a/src/helpers/transform.ts +++ b/src/helpers/transform.ts @@ -1,4 +1,5 @@ -import { Point2D } from "@/index"; +import { Point2D } from "@/types"; +import { point2d } from "@/utils"; export interface RotateOptions { target: Point2D; @@ -15,21 +16,19 @@ export type TransformOptions = TranslateOptions & RotateOptions; export function rotate(options: RotateOptions): Point2D { const { target, center, angle } = options; - const { cos, sin } = Math; + const cos = Math.cos(angle); + const sin = Math.sin(angle); const ox = target.x - center.x; const oy = target.y - center.y; - return { - x: center.x + (ox * cos(angle) - oy * sin(angle)), - y: center.y + (ox * sin(angle) + oy * cos(angle)), - }; + return point2d( + center.x + (ox * cos - oy * sin), + center.y + (ox * sin + oy * cos) + ); } export function translate(options: TranslateOptions) { const { target, translation } = options; - return { - x: target.x + translation.x, - y: target.y + translation.y, - }; + return point2d(target.x + translation.x, target.y + translation.y); } export function transform(options: TransformOptions) { diff --git a/src/svg/colors.ts b/src/svg/colors.ts new file mode 100644 index 00000000..f72a94fa --- /dev/null +++ b/src/svg/colors.ts @@ -0,0 +1,258 @@ +export const colors: string[] = [ + "#000000", + "#ff0000", + "#ffff00", + "#00ff00", + "#00ffff", + "#0000ff", + "#ff00ff", + "#ffffff", + "#414141", + "#808080", + "#ff0000", + "#ffaaaa", + "#bd0000", + "#bd7e7e", + "#810000", + "#815656", + "#680000", + "#684545", + "#4f0000", + "#4f3535", + "#ff3f00", + "#ffbfaa", + "#bd2e00", + "#bd8d7e", + "#811f00", + "#816056", + "#681900", + "#684e45", + "#4f1300", + "#4f3b35", + "#ff7f00", + "#ffd4aa", + "#bd5e00", + "#bd9d7e", + "#814000", + "#816b56", + "#683400", + "#685645", + "#4f2700", + "#4f4235", + "#ffbf00", + "#ffeaaa", + "#bd8d00", + "#bdad7e", + "#816000", + "#817656", + "#684e00", + "#685f45", + "#4f3b00", + "#4f4935", + "#ffff00", + "#ffffaa", + "#bdbd00", + "#bdbd7e", + "#818100", + "#818156", + "#686800", + "#686845", + "#4f4f00", + "#4f4f35", + "#bfff00", + "#eaffaa", + "#8dbd00", + "#adbd7e", + "#608100", + "#768156", + "#4e6800", + "#5f6845", + "#3b4f00", + "#494f35", + "#7fff00", + "#d4ffaa", + "#5ebd00", + "#9dbd7e", + "#408100", + "#6b8156", + "#346800", + "#566845", + "#274f00", + "#424f35", + "#3fff00", + "#bfffaa", + "#2ebd00", + "#8dbd7e", + "#1f8100", + "#608156", + "#196800", + "#4e6845", + "#134f00", + "#3b4f35", + "#00ff00", + "#aaffaa", + "#00bd00", + "#7ebd7e", + "#008100", + "#568156", + "#006800", + "#456845", + "#004f00", + "#354f35", + "#00ff3f", + "#aaffbf", + "#00bd2e", + "#7ebd8d", + "#00811f", + "#568160", + "#006819", + "#45684e", + "#004f13", + "#354f3b", + "#00ff7f", + "#aaffd4", + "#00bd5e", + "#7ebd9d", + "#008140", + "#56816b", + "#006834", + "#456856", + "#004f27", + "#354f42", + "#00ffbf", + "#aaffea", + "#00bd8d", + "#7ebdad", + "#008160", + "#568176", + "#00684e", + "#45685f", + "#004f3b", + "#354f49", + "#00ffff", + "#aaffff", + "#00bdbd", + "#7ebdbd", + "#008181", + "#568181", + "#006868", + "#456868", + "#004f4f", + "#354f4f", + "#00bfff", + "#aaeaff", + "#008dbd", + "#7eadbd", + "#006081", + "#567681", + "#004e68", + "#455f68", + "#003b4f", + "#35494f", + "#007fff", + "#aad4ff", + "#005ebd", + "#7e9dbd", + "#004081", + "#566b81", + "#003468", + "#455668", + "#00274f", + "#35424f", + "#003fff", + "#aabfff", + "#002ebd", + "#7e8dbd", + "#001f81", + "#566081", + "#001968", + "#454e68", + "#00134f", + "#353b4f", + "#0000ff", + "#aaaaff", + "#0000bd", + "#7e7ebd", + "#000081", + "#565681", + "#000068", + "#454568", + "#00004f", + "#35354f", + "#3f00ff", + "#bfaaff", + "#2e00bd", + "#8d7ebd", + "#1f0081", + "#605681", + "#190068", + "#4e4568", + "#13004f", + "#3b354f", + "#7f00ff", + "#d4aaff", + "#5e00bd", + "#9d7ebd", + "#400081", + "#6b5681", + "#340068", + "#564568", + "#27004f", + "#42354f", + "#bf00ff", + "#eaaaff", + "#8d00bd", + "#ad7ebd", + "#600081", + "#765681", + "#4e0068", + "#5f4568", + "#3b004f", + "#49354f", + "#ff00ff", + "#ffaaff", + "#bd00bd", + "#bd7ebd", + "#810081", + "#815681", + "#680068", + "#684568", + "#4f004f", + "#4f354f", + "#ff00bf", + "#ffaaea", + "#bd008d", + "#bd7ead", + "#810060", + "#815676", + "#68004e", + "#68455f", + "#4f003b", + "#4f3549", + "#ff007f", + "#ffaad4", + "#bd005e", + "#bd7e9d", + "#810040", + "#81566b", + "#680034", + "#684556", + "#4f0027", + "#4f3542", + "#ff003f", + "#ffaabf", + "#bd002e", + "#bd7e8d", + "#81001f", + "#815660", + "#680019", + "#68454e", + "#4f0013", + "#4f353b", + "#333333", + "#505050", + "#696969", + "#828282", + "#bebebe", + "#ffffff", +]; diff --git a/src/svg/elements.ts b/src/svg/elements.ts new file mode 100644 index 00000000..c6b85965 --- /dev/null +++ b/src/svg/elements.ts @@ -0,0 +1,91 @@ +import { Arc, Line, MText, Solid } from "@/entities"; +import { ArcPrimitive } from "@/helpers"; +import { LayerEntry } from "@/tables"; +import { colors } from "./colors"; + +export function lineSvg(line: Line, layer?: LayerEntry) { + const { start, end } = line; + const parts: string[] = []; + const color: string = colors[layer?.colorNumber ?? 0]; + parts.push(""); + return parts.join(" "); +} + +export function solidSvg(s: Solid, layer?: LayerEntry) { + const { first, second, third, fourth } = s; + const parts: string[] = []; + const color: string = colors[layer?.colorNumber ?? 0]; + parts.push(""); + return parts.join(" "); +} + +export function textSvg(t: MText, layer?: LayerEntry) { + const { insertionPoint: i } = t; + const parts: string[] = []; + const color: string = colors[layer?.colorNumber ?? 0]; + parts.push("${t.value}`); + return parts.join(" "); +} + +export function arcSvg(a: Arc, layer?: LayerEntry) { + const parts: string[] = []; + const color: string = colors[layer?.colorNumber ?? 0]; + parts.push(""); + return parts.join(" "); +} diff --git a/src/svg/exporter.ts b/src/svg/exporter.ts new file mode 100644 index 00000000..b94be8f4 --- /dev/null +++ b/src/svg/exporter.ts @@ -0,0 +1,67 @@ +import { arcSvg, lineSvg, solidSvg, textSvg } from "./elements"; +import { isArc, isDimension, isLine, isMText, isSolid } from "./guards"; +import { Block } from "@/blocks"; +import { Dimension } from "@/entities"; +import { Document } from "@/document"; + +const xmlns = "http://www.w3.org/2000/svg"; + +export class SVGExporter { + readonly doc: Document; + readonly lines: string[]; + + width: number; + height: number; + + private get _svg() { + const parts: string[] = ["`); + return parts.join(" "); + } + + constructor(doc: Document) { + this.doc = doc; + this.lines = []; + this.width = 2160; + this.height = 2160; + } + + layer(name?: string) { + return this.doc.tables.layer.get(name || "0"); + } + + start() { + const { doc, lines } = this; + this.clear(); + this.lines.push(this._svg); + this._block(doc.modelSpace); + lines.push(""); + } + + private _block(b: Block) { + b.entities.forEach((e) => { + const layer = this.layer(e.layerName); + if (isArc(e)) this.lines.push(arcSvg(e, layer)); + if (isLine(e)) this.lines.push(lineSvg(e, layer)); + if (isSolid(e)) this.lines.push(solidSvg(e, layer)); + if (isMText(e)) this.lines.push(textSvg(e, layer)); + if (isDimension(e)) this._dim(e); + }); + } + + private _dim(dim: Dimension) { + const b = this.doc.blocks.get(dim.blockName); + if (b) this._block(b); + } + + clear() { + this.lines.length = 0; + } + + stringify() { + return this.lines.join("\n"); + } +} diff --git a/src/svg/guards.ts b/src/svg/guards.ts new file mode 100644 index 00000000..4082b067 --- /dev/null +++ b/src/svg/guards.ts @@ -0,0 +1,21 @@ +import { Arc, Dimension, Entity, Line, MText, Solid } from "@/entities"; + +export function isLine(entity: Entity): entity is Line { + return entity instanceof Line; +} + +export function isSolid(entity: Entity): entity is Solid { + return entity instanceof Solid; +} + +export function isMText(entity: Entity): entity is MText { + return entity instanceof MText; +} + +export function isArc(entity: Entity): entity is Arc { + return entity instanceof Arc; +} + +export function isDimension(entity: Entity): entity is Dimension { + return entity instanceof Dimension; +} diff --git a/src/svg/index.ts b/src/svg/index.ts new file mode 100644 index 00000000..2e48daa6 --- /dev/null +++ b/src/svg/index.ts @@ -0,0 +1,8 @@ +import { Document } from "@/document"; +import { SVGExporter } from "./exporter"; + +export function svg(doc: Document) { + const exporter = new SVGExporter(doc); + exporter.start(); + return exporter.stringify(); +} diff --git a/src/tables/layer.ts b/src/tables/layer.ts index 4a078e23..f2c47783 100644 --- a/src/tables/layer.ts +++ b/src/tables/layer.ts @@ -1,4 +1,4 @@ -import { Colors, LineTypes, TagsManager } from "@/utils"; +import { Colors, LineTypes, TagsManager, onezero } from "@/utils"; import { OmitSeeder, WithSeeder } from "@/types"; import { Entry } from "./entry"; import { XTable } from "./table"; @@ -21,6 +21,7 @@ export interface LayerOptions extends WithSeeder { lineWeight?: number; materialObjectHandle?: string; trueColor?: number; + plot?: boolean; } export class LayerEntry extends Entry { @@ -28,11 +29,11 @@ export class LayerEntry extends Entry { flags: number; colorNumber: number; lineTypeName: string; - plottingFlag?: number; lineWeight?: number; plotStyleNameObjectHandle: string; materialObjectHandle?: string; trueColor?: number; + plot?: boolean; static readonly layerZeroName = "0"; @@ -46,6 +47,8 @@ export class LayerEntry extends Entry { this.plotStyleNameObjectHandle = "0"; this.materialObjectHandle = options.materialObjectHandle; this.trueColor = options.trueColor; + this.plot = options.plot; + if (this.name.toLocaleLowerCase() === "defpoints") this.plot = false; } override tagify(mg: TagsManager): void { @@ -56,6 +59,7 @@ export class LayerEntry extends Entry { mg.add(62, this.colorNumber); mg.add(420, this.trueColor); mg.add(6, this.lineTypeName); + mg.add(290, onezero(this.plot)); mg.add(370, this.lineWeight); mg.add(390, this.plotStyleNameObjectHandle); mg.add(347, this.materialObjectHandle); diff --git a/src/tables/tables.ts b/src/tables/tables.ts index 20f32d32..244daf7a 100644 --- a/src/tables/tables.ts +++ b/src/tables/tables.ts @@ -73,6 +73,12 @@ export class Tables implements Taggable, WithSeeder { return this.layer.add(options); } + addDefpointsLayer() { + const defpoints = this.layer.get("Defpoints"); + if (defpoints != null) return defpoints.name; + return this.layer.add({ name: "Defpoints", plot: false }).name; + } + addLType(options: OmitSeeder) { return this.ltype.add(options); } diff --git a/tsup.config.ts b/tsup.config.ts index a74261c8..10146076 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -4,7 +4,7 @@ type CustomOptions = Pick; function defineOptions({ format, dts, globalName }: CustomOptions): Options { return { - entry: ["./src/index.ts", "./src/helpers/index.ts"], + entry: ["./src/index.ts", "./src/helpers/index.ts", "./src/svg/index.ts"], outDir: "lib", format, dts,