@tracespace/plotter
Version:
Plot @tracespace/parser ASTs into image trees.
157 lines (133 loc) • 4.74 kB
text/typescript
import * as Tree from '../tree'
import type {Tool} from '../tool-store'
import {SIMPLE_TOOL} from '../tool-store'
import type {Location, Point} from '../location-store'
import {TWO_PI} from '../coordinate-math'
import {plotRectPath} from './plot-rect-path'
export const CW = 'cw'
export const CCW = 'ccw'
export type ArcDirection = typeof CW | typeof CCW
export function plotSegment(
location: Location,
arcDirection?: ArcDirection,
ambiguousArcCenter?: boolean
): Tree.PathSegment {
return arcDirection === undefined
? createLineSegment(location)
: createArcSegment(location, arcDirection, ambiguousArcCenter)
}
export function plotPath(
segments: Tree.PathSegment[],
tool: Tool | undefined,
region = false
): Tree.ImageGraphic | undefined {
if (segments.length > 0) {
if (region) {
return {type: Tree.IMAGE_REGION, segments}
}
if (tool?.type === SIMPLE_TOOL && tool.shape.type === Tree.CIRCLE) {
return {type: Tree.IMAGE_PATH, width: tool.shape.diameter, segments}
}
if (tool?.type === SIMPLE_TOOL && tool.shape.type === Tree.RECTANGLE) {
return plotRectPath(segments, tool.shape)
}
}
}
function createLineSegment(location: Location): Tree.PathLineSegment {
return {
type: Tree.LINE,
start: [location.startPoint.x, location.startPoint.y],
end: [location.endPoint.x, location.endPoint.y],
}
}
function createArcSegment(
location: Location,
arcDirection: ArcDirection,
ambiguousArcCenter = false
): Tree.PathSegment {
const {startPoint, endPoint, arcOffsets} = location
const radius =
arcOffsets.a > 0
? arcOffsets.a
: (arcOffsets.i ** 2 + arcOffsets.j ** 2) ** 0.5
if (ambiguousArcCenter || arcOffsets.a > 0) {
if (startPoint.x === endPoint.x && startPoint.y === endPoint.y) {
return createLineSegment(location)
}
// Get the center candidates and select the candidate with the smallest arc
const [start, end, center] = findCenterCandidates(location, radius)
.map(centerPoint => {
return getArcPositions(startPoint, endPoint, centerPoint, arcDirection)
})
.sort(([startA, endA], [startB, endB]) => {
const absSweepA = Math.abs(endA[2] - startA[2])
const absSweepB = Math.abs(endB[2] - startB[2])
return absSweepA - absSweepB
})[0]
return {type: Tree.ARC, start, end, center, radius}
}
const centerPoint = {
x: startPoint.x + arcOffsets.i,
y: startPoint.y + arcOffsets.j,
}
const [start, end, center] = getArcPositions(
startPoint,
endPoint,
centerPoint,
arcDirection
)
return {type: Tree.ARC, start, end, center, radius}
}
export function getArcPositions(
startPoint: Point,
endPoint: Point,
centerPoint: Point,
arcDirection: ArcDirection
): [start: Tree.ArcPosition, end: Tree.ArcPosition, center: Tree.Position] {
let startAngle = Math.atan2(
startPoint.y - centerPoint.y,
startPoint.x - centerPoint.x
)
let endAngle = Math.atan2(
endPoint.y - centerPoint.y,
endPoint.x - centerPoint.x
)
// If counter-clockwise, end angle should be greater than start angle
if (arcDirection === CCW) {
endAngle = endAngle > startAngle ? endAngle : endAngle + TWO_PI
} else {
startAngle = startAngle > endAngle ? startAngle : startAngle + TWO_PI
}
return [
[startPoint.x, startPoint.y, startAngle],
[endPoint.x, endPoint.y, endAngle],
[centerPoint.x, centerPoint.y],
]
}
// Find arc center candidates by finding the intersection points
// of two circles with `radius` centered on the start and end points
// https://math.stackexchange.com/a/1367732
function findCenterCandidates(location: Location, radius: number): Point[] {
// This function assumes that start and end are different points
const {x: x1, y: y1} = location.startPoint
const {x: x2, y: y2} = location.endPoint
// Distance between the start and end points
const [dx, dy] = [x2 - x1, y2 - y1]
const [sx, sy] = [x2 + x1, y2 + y1]
const distance = Math.sqrt(dx ** 2 + dy ** 2)
// If the distance to the midpoint equals the arc radius, then there is
// exactly one intersection at the midpoint; if the distance to the midpoint
// is greater than the radius, assume we've got a rounding error and just use
// the midpoint
if (radius <= distance / 2) {
return [{x: x1 + dx / 2, y: y1 + dy / 2}]
}
// No good name for these variables, but it's how the math works out
const factor = Math.sqrt((4 * radius ** 2) / distance ** 2 - 1)
const [xBase, yBase] = [sx / 2, sy / 2]
const [xAddend, yAddend] = [(dy * factor) / 2, (dx * factor) / 2]
return [
{x: xBase + xAddend, y: yBase - yAddend},
{x: xBase - xAddend, y: yBase + yAddend},
]
}