Skip to content

Commit

Permalink
feat: parabola perpendicular, bounding 92a9c85
Browse files Browse the repository at this point in the history
  • Loading branch information
plantain-00 committed Aug 11, 2024
1 parent 827cd2c commit 683e196
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 40 deletions.
44 changes: 29 additions & 15 deletions dev/cad-editor/plugins/parabola.plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,30 @@ export type ParabolaContent = model.BaseContent<'parabola'> & model.StrokeFields

export function getModel(ctx: PluginContext): model.Model<ParabolaContent> {
const ParabolaContent = ctx.and(ctx.BaseContent('parabola'), ctx.StrokeFields, ctx.ParabolaSegment, ctx.SegmentCountFields)
const getRefIds = (content: Omit<ParabolaContent, "type">): model.RefId[] => ctx.getStrokeRefIds(content)
const geometriesCache = new ctx.WeakmapValuesCache<Omit<ParabolaContent, "type">, model.BaseContent, model.Geometries<{ points: core.Position[] }>>()
function getParabolaGeometries(content: Omit<ParabolaContent, "type">, contents: readonly core.Nullable<model.BaseContent>[]) {
const refs = new Set(ctx.iterateRefContents(ctx.getStrokeRefIds(content), contents, [content]))
return geometriesCache.get(content, refs, () => {
const segmentCount = content.segmentCount ?? ctx.defaultSegmentCount
const rate = (content.t2 - content.t1) / segmentCount
const points: core.Position[] = []
const matrix = ctx.getCoordinateMatrix2D(content, ctx.getParabolaXAxisRadian(content))
for (let i = 0; i <= segmentCount; i++) {
const vec = ctx.getCoordinateVec2D(ctx.getParabolaCoordinatePointAtParam(content, content.t1 + i * rate))
const p = ctx.matrix.multiplyVec(matrix, vec)
points.push(ctx.vec2ToPosition(ctx.slice2(p)))
}
const lines: core.GeometryLine[] = [
// { type: 'parabola curve', curve: content },
]
return {
lines,
points,
bounding: ctx.getGeometryLinesBounding(lines),
renderingLines: ctx.dashedPolylineToLines(points, content.dashArray),
}
})
}
return {
type: 'parabola',
...ctx.strokeModel,
Expand All @@ -16,21 +39,12 @@ export function getModel(ctx: PluginContext): model.Model<ParabolaContent> {
},
render(content, renderCtx) {
const { options, target } = ctx.getStrokeRenderOptionsFromRenderContext(content, renderCtx)
const segmentCount = content.segmentCount ?? ctx.defaultSegmentCount
const rate = (content.t2 - content.t1) / segmentCount
const points: core.Position[] = []
const matrix = ctx.getCoordinateMatrix2D(content, ctx.angleToRadian(content.angle - 90))
for (let i = 0; i <= segmentCount; i++) {
const x = content.t1 + i * rate
const y = 2 * content.p * x ** 2
const vec = ctx.getCoordinateVec2D({ x, y })
const p = ctx.matrix.multiplyVec(matrix, vec)
points.push({ x: p[0], y: p[1] })
}
const { points } = getParabolaGeometries(content, renderCtx.contents)
return target.renderPolyline(points, options)
},
getGeometries: getParabolaGeometries,
isValid: (c, p) => ctx.validate(c, ParabolaContent, p),
getRefIds,
getRefIds: ctx.getStrokeRefIds,
updateRefId: ctx.updateStrokeRefIds,
deleteRefId: ctx.deleteStrokeRefIds,
}
Expand Down Expand Up @@ -103,7 +117,7 @@ export function getCommand(ctx: PluginContext): Command {
angle: ctx.radianToAngle(ctx.getTwoPointsRadian(p, content)),
})
} else if (status === 't1') {
const x = ctx.getPerpendicularParamToLine2D(p, content, ctx.angleToRadian(content.angle - 90))
const x = ctx.getPerpendicularParamToLine2D(p, content, ctx.getParabolaXAxisRadian(content))
const y = ctx.getPerpendicularParamToLine2D(p, content, ctx.angleToRadian(content.angle))
setContent({
...content,
Expand All @@ -113,7 +127,7 @@ export function getCommand(ctx: PluginContext): Command {
} else if (status === 't2') {
setContent({
...content,
t2: ctx.getPerpendicularParamToLine2D(p, content, ctx.angleToRadian(content.angle - 90)),
t2: ctx.minimumBy(ctx.getPerpendicularParamsToParabola(p, content), t => ctx.getTwoPointsDistanceSquare(p, ctx.getParabolaPointAtParam(content, t))),
})
}
},
Expand Down
46 changes: 30 additions & 16 deletions dev/cad-editor/plugins/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7486,31 +7486,45 @@ export {
`// dev/cad-editor/plugins/parabola.plugin.tsx
function getModel(ctx) {
const ParabolaContent = ctx.and(ctx.BaseContent("parabola"), ctx.StrokeFields, ctx.ParabolaSegment, ctx.SegmentCountFields);
const getRefIds = (content) => ctx.getStrokeRefIds(content);
const geometriesCache = new ctx.WeakmapValuesCache();
function getParabolaGeometries(content, contents) {
const refs = new Set(ctx.iterateRefContents(ctx.getStrokeRefIds(content), contents, [content]));
return geometriesCache.get(content, refs, () => {
var _a;
const segmentCount = (_a = content.segmentCount) != null ? _a : ctx.defaultSegmentCount;
const rate = (content.t2 - content.t1) / segmentCount;
const points = [];
const matrix = ctx.getCoordinateMatrix2D(content, ctx.getParabolaXAxisRadian(content));
for (let i = 0; i <= segmentCount; i++) {
const vec = ctx.getCoordinateVec2D(ctx.getParabolaCoordinatePointAtParam(content, content.t1 + i * rate));
const p = ctx.matrix.multiplyVec(matrix, vec);
points.push(ctx.vec2ToPosition(ctx.slice2(p)));
}
const lines = [
// { type: 'parabola curve', curve: content },
];
return {
lines,
points,
bounding: ctx.getGeometryLinesBounding(lines),
renderingLines: ctx.dashedPolylineToLines(points, content.dashArray)
};
});
}
return {
type: "parabola",
...ctx.strokeModel,
move(content, offset) {
ctx.movePoint(content, offset);
},
render(content, renderCtx) {
var _a;
const { options, target } = ctx.getStrokeRenderOptionsFromRenderContext(content, renderCtx);
const segmentCount = (_a = content.segmentCount) != null ? _a : ctx.defaultSegmentCount;
const rate = (content.t2 - content.t1) / segmentCount;
const points = [];
const matrix = ctx.getCoordinateMatrix2D(content, ctx.angleToRadian(content.angle - 90));
for (let i = 0; i <= segmentCount; i++) {
const x = content.t1 + i * rate;
const y = 2 * content.p * x ** 2;
const vec = ctx.getCoordinateVec2D({ x, y });
const p = ctx.matrix.multiplyVec(matrix, vec);
points.push({ x: p[0], y: p[1] });
}
const { points } = getParabolaGeometries(content, renderCtx.contents);
return target.renderPolyline(points, options);
},
getGeometries: getParabolaGeometries,
isValid: (c, p) => ctx.validate(c, ParabolaContent, p),
getRefIds,
getRefIds: ctx.getStrokeRefIds,
updateRefId: ctx.updateStrokeRefIds,
deleteRefId: ctx.deleteStrokeRefIds
};
Expand Down Expand Up @@ -7576,7 +7590,7 @@ function getCommand(ctx) {
angle: ctx.radianToAngle(ctx.getTwoPointsRadian(p, content))
});
} else if (status === "t1") {
const x = ctx.getPerpendicularParamToLine2D(p, content, ctx.angleToRadian(content.angle - 90));
const x = ctx.getPerpendicularParamToLine2D(p, content, ctx.getParabolaXAxisRadian(content));
const y = ctx.getPerpendicularParamToLine2D(p, content, ctx.angleToRadian(content.angle));
setContent({
...content,
Expand All @@ -7586,7 +7600,7 @@ function getCommand(ctx) {
} else if (status === "t2") {
setContent({
...content,
t2: ctx.getPerpendicularParamToLine2D(p, content, ctx.angleToRadian(content.angle - 90))
t2: ctx.minimumBy(ctx.getPerpendicularParamsToParabola(p, content), (t) => ctx.getTwoPointsDistanceSquare(p, ctx.getParabolaPointAtParam(content, t)))
});
}
},
Expand Down
2 changes: 1 addition & 1 deletion main.bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package-size.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
composable-type-validator: 5.24e+3
react-render-target: 1.65e+5
react-render-target: 1.66e+5
use-undo-redo: 1.61e+3
use-patch-based-undo-redo: 4.10e+3
react-composable-json-editor: 2.00e+4
Expand Down
25 changes: 20 additions & 5 deletions src/utils/ellipse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getPointByLengthAndRadian, rotatePositionByCenter } from "./position";
import { getPointByLengthAndRadian, rotatePositionByCenter, vec2ToPosition } from "./position";
import { AngleRange } from "./angle";
import { rotatePosition } from "./position";
import { getDirectionByRadian } from "./radian";
Expand All @@ -11,7 +11,9 @@ import { number, minimum, optional, and } from "./validators";
import { calculateEquation2 } from "./equation-calculater";
import { getPointSideOfLine, twoPointLineToGeneralFormLine } from "./line";
import { Arc } from "./circle";
import { Tuple5 } from "./types";
import { slice2, Tuple5, Vec3 } from "./types";
import { getCoordinateMatrix2D } from "./transform";
import { matrix } from "./matrix";

export function pointIsOnEllipseArc(p: Position, ellipseArc: EllipseArc, extend: ExtendType = { body: true }) {
if (extend.head && extend.body && extend.tail) return true
Expand Down Expand Up @@ -100,15 +102,28 @@ export function getEllipsePointAtRadian(content: Ellipse, radian: number) {
export function ellipseToPolygon(content: Ellipse, angleDelta: number) {
const lineSegmentCount = 360 / angleDelta
const points: Position[] = []
const m = getCoordinateMatrix2D(getEllipseCenter(content), angleToRadian(content.angle))
for (let i = 0; i < lineSegmentCount; i++) {
const radian = angleToRadian(angleDelta * i)
points.push(getEllipsePointAtRadian(content, radian))
const vec = getEllipseCoordinateVec2DAtRadian(content, radian)
const p = matrix.multiplyVec(m, vec)
points.push(vec2ToPosition(slice2(p)))
}
return points
}

export function ellipseArcToPolyline(content: EllipseArc, angleDelta: number) {
return getAngleRange(content, angleDelta).map(i => getEllipseArcPointAtAngle(content, i))
export function ellipseArcToPolyline(content: EllipseArc, angleDelta: number): Position[] {
const m = getCoordinateMatrix2D(getEllipseCenter(content), angleToRadian(content.angle))
return getAngleRange(content, angleDelta).map(i => {
const vec = getEllipseCoordinateVec2DAtRadian(content, angleToRadian(i))
const p = matrix.multiplyVec(m, vec)
return vec2ToPosition(slice2(p))
})
}

export function getEllipseCoordinateVec2DAtRadian(content: Ellipse, radian: number): Vec3 {
const direction = getDirectionByRadian(radian)
return [content.rx * direction.x, content.ry * direction.y, 1]
}

export function getEllipseArcPointAtAngle(content: EllipseArc, angle: number) {
Expand Down
89 changes: 88 additions & 1 deletion src/utils/parabola.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Position } from "./position"
import { getPointsBoundingUnsafe } from "./bounding"
import { calculateEquation3 } from "./equation-calculater"
import { delta2, isBetween, isZero, minimumBy } from "./math"
import { getTwoPointsDistance, Position } from "./position"
import { angleToRadian } from "./radian"
import { TwoPointsFormRegion } from "./region"
import { transformPointFromCoordinate2D } from "./transform"
import { and, number } from "./validators"

export interface Parabola extends Position {
Expand All @@ -20,3 +26,84 @@ export const ParabolaSegment = /* @__PURE__ */ and(Parabola, {
t1: number,
t2: number,
})

export function getPerpendicularParamsToParabola({ x: x0, y: y0 }: Position, { p, angle, x: x1, y: y1 }: Parabola, delta = delta2): number[] {
const xAxisRadian = getParabolaXAxisRadian({ angle })
const e1 = Math.sin(xAxisRadian), e2 = Math.cos(xAxisRadian)
// x = x1 + e2 t - 2 e1 p t^2
// y = y1 + e1 t + 2 e2 p t^2
// x' = e2 - 4 e1 p t
// y' = 4 e2 p t + e1
// y'/x'*(y - y0)/(x - x0) = -1
// y'(y - y0) = -x'(x - x0)
// x'(x - x0) + y'(y - y0) = 0
// (e2 - 4 e1 p t)(x1 + e2 t - 2 e1 p t^2 - x0) + (4 e2 p t + e1)(y1 + e1 t + 2 e2 p t^2 - y0) = 0
const a1 = x1 - x0, a2 = y1 - y0
// (e2 - 4 e1 p t)(a1 + e2 t - 2 e1 p t^2) + (4 e2 p t + e1)(a2 + e1 t + 2 e2 p t^2) = 0
// (8 e1 e1 p p + 8 e2 e2 p p) t t t + (-4 a1 e1 p + e1 e1 + e2 e2 + 4 a2 e2 p) t + a1 e2 + a2 e1 = 0
// 8 p p t t t + (4 p (a2 e2 - a1 e1) + 1) t + a1 e2 + a2 e1 = 0
return calculateEquation3(
8 * p * p,
0,
4 * p * (a2 * e2 - a1 * e1) + 1,
a1 * e2 + a2 * e1,
delta,
)
}

export function getParabolaPointAtParam(parabola: Parabola, param: number): Position {
return transformPointFromCoordinate2D(getParabolaCoordinatePointAtParam(parabola, param), parabola, getParabolaXAxisRadian(parabola))
}

export function getParabolaCoordinatePointAtParam(parabola: Parabola, param: number): Position {
return { x: param, y: 2 * parabola.p * param ** 2 }
}

export function getParabolaXAxisRadian(parabola: Pick<Parabola, 'angle'>): number {
return angleToRadian(parabola.angle - 90)
}

export function getPointAndParabolaNearestPointAndDistance(position: Position, curve: ParabolaSegment, extend = false) {
let us = getPerpendicularParamsToParabola(position, curve)
if (!extend) {
us = us.filter(u => isBetween(u, curve.t1, curve.t2))
us.push(curve.t1, curve.t2)
}
const points = us.map(u => ({
u,
p: getParabolaPointAtParam(curve, u),
}))
const results = points.map(p => ({
percent: p.u,
point: p.p,
distance: getTwoPointsDistance(position, p.p)
}))
return minimumBy(results, v => v.distance)
}

export function getParabolaBounding(curve: ParabolaSegment): TwoPointsFormRegion {
const { p, angle } = curve
const xAxisRadian = getParabolaXAxisRadian({ angle })
const e1 = Math.sin(xAxisRadian), e2 = Math.cos(xAxisRadian)
// x = x1 + e2 t - 2 e1 p t^2
// y = y1 + e1 t + 2 e2 p t^2
// x' = e2 - 4 e1 p t = 0
// y' = 4 e2 p t + e1 = 0
const points = [
getParabolaPointAtParam(curve, curve.t1),
getParabolaPointAtParam(curve, curve.t2),
]
if (!isZero(e1)) {
const t = e2 / 4 / e1 / p
if (isBetween(t, curve.t1, curve.t2)) {
points.push(getParabolaPointAtParam(curve, t))
}
}
if (!isZero(e2)) {
const t = - e1 / 4 / e2 / p
if (isBetween(t, curve.t1, curve.t2)) {
points.push(getParabolaPointAtParam(curve, t))
}
}
return getPointsBoundingUnsafe(points)
}
13 changes: 12 additions & 1 deletion src/utils/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isSameNumber } from "./math";
import { isZero } from "./math";
import { v3 } from "./matrix";
import { angleToRadian, getDirectionByRadian } from "./radian";
import { Vec3 } from "./types";
import { Vec2, Vec3 } from "./types";
import { and, number } from "./validators";

export interface Position {
Expand Down Expand Up @@ -38,6 +38,17 @@ export function vec3ToPosition3D(vec: Vec3): Position3D {
}
}

export function positionToVec2(p: Position): Vec2 {
return [p.x, p.y]
}

export function vec2ToPosition(vec: Vec2): Position {
return {
x: vec[0],
y: vec[1],
}
}

export function getPointByLengthAndDirection(
startPoint: Position,
length: number,
Expand Down
4 changes: 4 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export type Tuple3<T> = [T, T, T]
export type Tuple4<T> = [T, T, T, T]
export type Tuple5<T> = [T, T, T, T, T]

export function slice2<T>(array: { [index: number]: T }, start = 0): Tuple2<T> {
return [array[start], array[start + 1]]
}

export function slice3<T>(array: { [index: number]: T }, start = 0): Tuple3<T> {
return [array[start], array[start + 1], array[start + 2]]
}

0 comments on commit 683e196

Please sign in to comment.