Skip to content

Commit

Permalink
adding csg module and example
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewjacobson committed Jul 10, 2024
1 parent b57c205 commit d5fd5d3
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 2 deletions.
77 changes: 77 additions & 0 deletions examples/csg.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!doctype html>
<html>
<head>
<title>CSG Example</title>
<script src="../dist/stitch.global.js"></script>
</head>
<body>
<script>

// set up the RNG
let R = new Stitch.Math.Random();

// create a new pattern
let pattern = new Stitch.Core.Pattern(500, 500);

// add the thread
let blackThread = pattern.addThread(0, 0, 0);
let redThread = pattern.addThread(255, 0, 0);

// create the CSG shapes
let countCircles = 20;
let csgShapes = [];
for (let i = 0; i < countCircles; i++) {
let radius = R.random_num(0.1 * pattern.widthPx, 0.2 * pattern.widthPx);
let cx = R.random_num(radius, pattern.widthPx - radius);
let cy = R.random_num(radius, pattern.heightPx - radius);
let polyline = new Stitch.Math.Polyline(true);
for (let a = 0; a < 2 * Math.PI; a += 0.1) polyline.addVertex(radius * Math.cos(a) + cx, radius * Math.sin(a) + cy);
csgShapes.push(Stitch.CSG.Shape.fromPolylines([polyline.getSimplified(1)]));
}

// perform the polygon clipping
let blackShape = csgShapes[0].subtract(csgShapes[1]);
let redShape = csgShapes[1].subtract(csgShapes[0]);
for (let i = 2; i < csgShapes.length; i++) {
blackShape = (i % 2 === 1) ? blackShape.subtract(csgShapes[i]) : blackShape.union(csgShapes[i]);
redShape = (i % 2 === 0) ? redShape.subtract(csgShapes[i]) : redShape.union(csgShapes[i]);
}

let blackPolylineShapes = blackShape.toPolylines();
for (let polylineShape of blackPolylineShapes) {
if (polylineShape.polyline.vertices.length > 3) {
blackThread.addRun(new Stitch.Runs.Run(polylineShape.polyline, 1));
}
blackThread.addRun(new Stitch.Runs.AutoFill([polylineShape.polyline, ...polylineShape.contours], R.random_num(0, Math.PI), 0.2, 3, new Stitch.Math.Vector(0, 0), new Stitch.Math.Vector(0, 0)));
}

let redPolylineShapes = redShape.toPolylines();
for (let polylineShape of redPolylineShapes) {
if (polylineShape.polyline.vertices.length > 3) {
redThread.addRun(new Stitch.Runs.Run(polylineShape.polyline, 1));
}
redThread.addRun(new Stitch.Runs.AutoFill([polylineShape.polyline, ...polylineShape.contours], R.random_num(0, Math.PI), 0.2, 3, new Stitch.Math.Vector(0, 0), new Stitch.Math.Vector(0, 0)));
}

let svg = Stitch.Graphics.getSvg(pattern, window.innerWidth, window.innerHeight);
svg.setAttribute('style', 'margin: auto; position: absolute; inset: 0;');
document.body.append(svg);

let modal = Stitch.Browser.Modal.createDownloadModal(pattern, 'csg', 10, 500);
document.body.appendChild(modal.container);
window.addEventListener('click', (e) => {
if (e.target === modal.container) modal.close();
});
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyD') modal.open();
});

window.addEventListener("resize", Stitch.Browser.Utils.debounce(() => {
svg.remove();
svg = Stitch.Graphics.getSvg(pattern, window.innerWidth, window.innerHeight);
svg.setAttribute('style', 'margin: auto; position: absolute; inset: 0;');
document.body.append(svg);
}, 10));
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@stitchables/stitchjs",
"license": "MIT",
"version": "0.0.3",
"version": "0.0.4",
"main": "dist/stitch.js",
"module": "dist/stitch.mjs",
"types": "dist/stitch.d.ts",
Expand Down
80 changes: 80 additions & 0 deletions src/CSG/Line.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Vector } from '../Math/Vector';
import { Segment } from './Segment';

export class Line {
origin: Vector;
direction: Vector;
normal: Vector;
constructor(origin: Vector, direction: Vector) {
this.origin = origin;
this.direction = direction.normalized();
this.normal = new Vector(this.direction.y, -1 * this.direction.x);
}
static fromPoints(a: Vector, b: Vector): Line {
return new Line(a, b.subtract(a).normalized());
}
clone(): Line {
return new Line(this.origin.copy(), this.direction.copy());
}
flip(): void {
this.direction = this.direction.multiply(-1);
this.normal = this.normal.multiply(-1);
}
splitSegment(
segment: Segment,
collinearRight: Segment[],
collinearLeft: Segment[],
right: Segment[],
left: Segment[],
epsilon = 1e-5,
): void {
const [COLLINEAR, RIGHT, LEFT, SPANNING] = [0, 1, 2, 3];

let segmentType = 0;
let t = 0;
const types: number[] = [];
for (let i = 0; i < segment.vertices.length; i++) {
t = this.normal.dot(segment.vertices[i].subtract(this.origin));
const type = t < -epsilon ? RIGHT : t > epsilon ? LEFT : COLLINEAR;
segmentType |= type;
types.push(type);
}

switch (segmentType) {
case COLLINEAR:
if (t > 0 || segment.line.origin.x >= this.origin.x) collinearRight.push(segment);
else collinearLeft.push(segment);
break;
case RIGHT:
right.push(segment);
break;
case LEFT:
left.push(segment);
break;
case SPANNING:
const [r, l]: [Vector[], Vector[]] = [[], []];
const [ti, tj] = [types[0], types[1]];
const [vi, vj] = [segment.vertices[0], segment.vertices[1]];
if (ti === RIGHT && tj === RIGHT) {
r.push(vi, vj);
} else if (ti === LEFT && tj === LEFT) {
l.push(vi, vj);
} else if (ti === RIGHT && tj === LEFT) {
const tw =
this.normal.dot(this.origin.subtract(vi)) / this.normal.dot(vj.subtract(vi));
const v = vi.lerp(vj, tw);
r.push(vi, v);
l.push(v.copy(), vj);
} else if (ti === LEFT && tj === RIGHT) {
const tw =
this.normal.dot(this.origin.subtract(vi)) / this.normal.dot(vj.subtract(vi));
const v = vi.lerp(vj, tw);
l.push(vi, v);
r.push(v.copy(), vj);
}
if (r.length >= 2) right.push(new Segment(r));
if (l.length >= 2) left.push(new Segment(l));
break;
}
}
}
67 changes: 67 additions & 0 deletions src/CSG/Node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Segment } from './Segment';
import { Line } from './Line';

export class Node {
line: Line | undefined;
right: Node | undefined;
left: Node | undefined;
segments: Segment[];
constructor(segments?: Segment[]) {
this.segments = [];
if (segments) this.build(segments);
}
clone(): Node {
const node = new Node();
if (this.line) node.line = this.line.clone();
if (this.right) node.right = this.right.clone();
if (this.left) node.left = this.left.clone();
node.segments = this.segments.map((p) => p.clone());
return node;
}
invert(): void {
for (let i = 0; i < this.segments.length; i++) this.segments[i].flip();
if (this.line) this.line.flip();
if (this.right) this.right.invert();
if (this.left) this.left.invert();
[this.right, this.left] = [this.left, this.right];
}
clipSegments(segments: Segment[]): Segment[] {
if (!this.line) return segments.slice();
let right: Segment[] = [];
let left: Segment[] = [];
for (let i = 0; i < segments.length; i++) {
this.line.splitSegment(segments[i], right, left, right, left);
}
if (this.right) right = this.right.clipSegments(right);
if (this.left) left = this.left.clipSegments(left);
else left = [];
return right.concat(left);
}
clipTo(bsp: Node): void {
this.segments = bsp.clipSegments(this.segments);
if (this.right) this.right.clipTo(bsp);
if (this.left) this.left.clipTo(bsp);
}
allSegments(): Segment[] {
let segments = this.segments.slice();
if (this.right) segments = segments.concat(this.right.allSegments());
if (this.left) segments = segments.concat(this.left.allSegments());
return segments;
}
build(segments: Segment[]): void {
if (!segments || segments.length < 1) return;
if (!this.line) this.line = segments[0].line.clone();
let [right, left] = [[], []];
for (let i = 0; i < segments.length; i++) {
this.line.splitSegment(segments[i], this.segments, this.segments, right, left);
}
if (right.length > 0) {
if (!this.right) this.right = new Node();
this.right.build(right);
}
if (left.length > 0) {
if (!this.left) this.left = new Node();
this.left.build(left);
}
}
}
18 changes: 18 additions & 0 deletions src/CSG/Segment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Vector } from '../Math/Vector';
import { Line } from './Line';

export class Segment {
vertices: Vector[];
line: Line;
constructor(vertices: Vector[]) {
this.vertices = vertices;
this.line = Line.fromPoints(vertices[0], vertices[1]);
}
clone(): Segment {
return new Segment(this.vertices.map((v) => v.copy()));
}
flip(): void {
this.vertices.reverse().map((v) => v.multiply(-1));
this.line.flip();
}
}
Loading

0 comments on commit d5fd5d3

Please sign in to comment.