From b953f6e1675486f45d3369a0d714c7166f90a1d2 Mon Sep 17 00:00:00 2001 From: york Date: Thu, 14 Nov 2024 07:11:32 +0800 Subject: [PATCH] feat: get circle tangent to 2 geometry lines near param --- ...e-tangent-tangent-radius-circle.plugin.tsx | 110 +++++++++++++++++- dev/cad-editor/plugins/variables.ts | 101 ++++++++++++++++ src/utils/index.ts | 1 + src/utils/tangent-circle.ts | 79 +++++++++++++ 4 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 src/utils/tangent-circle.ts diff --git a/dev/cad-editor/plugins/create-tangent-tangent-radius-circle.plugin.tsx b/dev/cad-editor/plugins/create-tangent-tangent-radius-circle.plugin.tsx index 7bdb7dff..46aa2c4b 100644 --- a/dev/cad-editor/plugins/create-tangent-tangent-radius-circle.plugin.tsx +++ b/dev/cad-editor/plugins/create-tangent-tangent-radius-circle.plugin.tsx @@ -2,7 +2,7 @@ import type { PluginContext } from './types' import type * as core from '../../../src' import type { Command } from '../command' import type * as model from '../model' -import { isLineContent } from './line-polyline.plugin' +import { isLineContent, LineContent } from './line-polyline.plugin' import { CircleContent, isArcContent, isCircleContent } from './circle-arc.plugin' export function getCommand(ctx: PluginContext): Command[] { @@ -38,6 +38,15 @@ export function getCommand(ctx: PluginContext): Command[] { ) const contentSelectable = (c: model.BaseContent) => isCircleContent(c) || isArcContent(c) || isLineContent(c) + const icon2 = ( + + + + + + + + ) const command: Command = { name: 'create tangent tangent radius circle', useCommand({ onEnd, type, selected, scale }) { @@ -213,6 +222,105 @@ export function getCommand(ctx: PluginContext): Command[] { } }, selectCount: 0, + }, + { + name: 'create tangent tangent radius circle at points', + useCommand({ onEnd, type, scale, contents }) { + const [start, setStart] = React.useState<{ point: core.Position, param: number, line: core.GeometryLine }>() + const [cursor, setCursor] = React.useState() + const [radius, setRadius] = React.useState() + const [result, setResult] = React.useState() + const assistentContents: (LineContent | CircleContent)[] = [] + if (start && cursor && type) { + assistentContents.push({ + points: [start.point, cursor], + type: 'line', + dashArray: [4 / scale], + }) + } + if (result) { + assistentContents.push({ ...result, type: 'circle' } as CircleContent) + } + const getTarget = (point: core.Position, id: number, param: number) => { + const content = contents[id] + if (!content) return + const lines = ctx.getContentModel(content)?.getGeometries?.(content, contents)?.lines + if (!lines) return + const index = Math.floor(param) + return { point, line: lines[index], param: param - index } + } + let message = '' + if (type && !radius) { + message = 'input radius' + } + const { input, setInputPosition, setCursorPosition, clearText, resetInput } = ctx.useCursorInput(message, type ? (e, text) => { + if (e.key === 'Enter') { + const r = +text + if (!isNaN(r)) { + setRadius(r) + clearText() + resetInput() + } + } + } : undefined) + const reset = () => { + setStart(undefined) + setResult(undefined) + setCursor(undefined) + setRadius(undefined) + clearText() + resetInput() + } + return { + onStart(p, target) { + if (!type) return + if (!target) return + if (target.param === undefined) return + if (!start) { + setStart(getTarget(p, target.id, target.param)) + } else if (result) { + onEnd({ + updateContents: (contents) => { + contents.push({ ...result, type: 'circle' } as CircleContent) + } + }) + reset() + } + }, + onMove(p, viewportPosition, target) { + if (!type) return + setCursor(p) + setResult(undefined) + setCursorPosition(p) + setInputPosition(viewportPosition || p) + if (!radius) return + if (!target) return + if (target.param === undefined) return + if (!start) return + const end = getTarget(p, target.id, target.param) + if (!end) return + const center = ctx.getTwoGeneralFormLinesIntersectionPoint( + ctx.pointAndDirectionToGeneralFormLine( + start.point, + ctx.getGeometryLineTangentRadianAtParam(start.param, start.line), + ), + ctx.pointAndDirectionToGeneralFormLine( + end.point, + ctx.getGeometryLineTangentRadianAtParam(end.param, end.line), + ), + ) + if (!center) return + const circle = ctx.getCircleTangentTo2GeometryLinesNearParam(start.line, end.line, radius, start.param, end.param, center) + if (!circle) return + setResult({ ...circle, r: radius }) + }, + input, + assistentContents, + reset, + } + }, + selectCount: 0, + icon: icon2, } ] } diff --git a/dev/cad-editor/plugins/variables.ts b/dev/cad-editor/plugins/variables.ts index 193ac84d..fc56bd45 100644 --- a/dev/cad-editor/plugins/variables.ts +++ b/dev/cad-editor/plugins/variables.ts @@ -2733,6 +2733,7 @@ function getCommand(ctx) { const React = ctx.React; const icon = /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 100 100" }, /* @__PURE__ */ React.createElement("polyline", { points: "10,87 89,87", strokeWidth: "5", strokeMiterlimit: "10", strokeLinejoin: "miter", strokeLinecap: "butt", fill: "none", stroke: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "17", cy: "40", r: "16", strokeWidth: "5", strokeMiterlimit: "10", strokeLinejoin: "miter", strokeLinecap: "butt", fill: "none", stroke: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "60", cy: "57", r: "30", strokeWidth: "5", strokeMiterlimit: "10", strokeLinejoin: "miter", strokeLinecap: "butt", fill: "none", stroke: "currentColor" })); const contentSelectable = (c) => isCircleContent(c) || isArcContent(c) || isLineContent(c); + const icon2 = /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 100 100" }, /* @__PURE__ */ React.createElement("polyline", { points: "10,87 89,87", strokeWidth: "5", strokeMiterlimit: "10", strokeLinejoin: "miter", strokeLinecap: "butt", fill: "none", stroke: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "17", cy: "40", r: "16", strokeWidth: "5", strokeMiterlimit: "10", strokeLinejoin: "miter", strokeLinecap: "butt", fill: "none", stroke: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "60", cy: "57", r: "30", strokeWidth: "5", strokeMiterlimit: "10", strokeLinejoin: "miter", strokeLinecap: "butt", fill: "none", stroke: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "33", cy: "46", r: "8", fillOpacity: "1", strokeOpacity: "1", fill: "currentColor", stroke: "none" }), /* @__PURE__ */ React.createElement("circle", { cx: "60", cy: "87", r: "8", fillOpacity: "1", strokeOpacity: "1", fill: "currentColor", stroke: "none" })); const command = { name: "create tangent tangent radius circle", useCommand({ onEnd, type, selected, scale }) { @@ -2907,6 +2908,106 @@ function getCommand(ctx) { }; }, selectCount: 0 + }, + { + name: "create tangent tangent radius circle at points", + useCommand({ onEnd, type, scale, contents }) { + const [start, setStart] = React.useState(); + const [cursor, setCursor] = React.useState(); + const [radius, setRadius] = React.useState(); + const [result, setResult] = React.useState(); + const assistentContents = []; + if (start && cursor && type) { + assistentContents.push({ + points: [start.point, cursor], + type: "line", + dashArray: [4 / scale] + }); + } + if (result) { + assistentContents.push({ ...result, type: "circle" }); + } + const getTarget = (point, id, param) => { + var _a, _b, _c; + const content = contents[id]; + if (!content) return; + const lines = (_c = (_b = (_a = ctx.getContentModel(content)) == null ? void 0 : _a.getGeometries) == null ? void 0 : _b.call(_a, content, contents)) == null ? void 0 : _c.lines; + if (!lines) return; + const index = Math.floor(param); + return { point, line: lines[index], param: param - index }; + }; + let message = ""; + if (type && !radius) { + message = "input radius"; + } + const { input, setInputPosition, setCursorPosition, clearText, resetInput } = ctx.useCursorInput(message, type ? (e, text) => { + if (e.key === "Enter") { + const r = +text; + if (!isNaN(r)) { + setRadius(r); + clearText(); + resetInput(); + } + } + } : void 0); + const reset = () => { + setStart(void 0); + setResult(void 0); + setCursor(void 0); + setRadius(void 0); + clearText(); + resetInput(); + }; + return { + onStart(p, target) { + if (!type) return; + if (!target) return; + if (target.param === void 0) return; + if (!start) { + setStart(getTarget(p, target.id, target.param)); + } else if (result) { + onEnd({ + updateContents: (contents2) => { + contents2.push({ ...result, type: "circle" }); + } + }); + reset(); + } + }, + onMove(p, viewportPosition, target) { + if (!type) return; + setCursor(p); + setResult(void 0); + setCursorPosition(p); + setInputPosition(viewportPosition || p); + if (!radius) return; + if (!target) return; + if (target.param === void 0) return; + if (!start) return; + const end = getTarget(p, target.id, target.param); + if (!end) return; + const center = ctx.getTwoGeneralFormLinesIntersectionPoint( + ctx.pointAndDirectionToGeneralFormLine( + start.point, + ctx.getGeometryLineTangentRadianAtParam(start.param, start.line) + ), + ctx.pointAndDirectionToGeneralFormLine( + end.point, + ctx.getGeometryLineTangentRadianAtParam(end.param, end.line) + ) + ); + if (!center) return; + const circle = ctx.getCircleTangentTo2GeometryLinesNearParam(start.line, end.line, radius, start.param, end.param, center); + if (!circle) return; + setResult({ ...circle, r: radius }); + }, + input, + assistentContents, + reset + }; + }, + selectCount: 0, + icon: icon2 } ]; } diff --git a/src/utils/index.ts b/src/utils/index.ts index c1cf4c01..335a6900 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -66,3 +66,4 @@ export * from './cone' export * from './parabola' export * from './hyperbola' export * from './tangent-line' +export * from './tangent-circle' diff --git a/src/utils/tangent-circle.ts b/src/utils/tangent-circle.ts new file mode 100644 index 00000000..6dcfc5da --- /dev/null +++ b/src/utils/tangent-circle.ts @@ -0,0 +1,79 @@ +import { newtonIterates } from "./equation-calculater"; +import { GeometryLine, getGeometryLineDerivatives } from "./geometry-line"; +import { delta2 } from "./math"; +import { Position } from "./position"; + +export function getCircleTangentTo2GeometryLinesNearParam(curve1: GeometryLine, curve2: GeometryLine, radius: number, t1: number, t2: number, center: Position): Position | undefined { + const derivative1 = getGeometryLineDerivatives(curve1) + const derivative2 = getGeometryLineDerivatives(curve2) + const r = radius ** 2 + const f1 = (t: number[]): number[] => { + const x = t[2], y = t[3] + // (y - y1)/(x - x1)y1'/x1' = -1 + // (y - y2)/(x - x2)y2'/x2' = -1 + // (x - x1)^2 + (y - y1)^2 = r + // (x - x2)^2 + (y - y2)^2 = r + // z1 = (x - x1)x1' + (y - y1)y1' + // z2 = (x - x2)x2' + (y - y2)y2' + // z3 = (x - x1)^2 + (y - y1)^2 - r + // z4 = (x - x2)^2 + (y - y2)^2 - r + const [{ x: x1, y: y1 }, { x: x11, y: y11 }] = derivative1[0](t[0]) + const [{ x: x2, y: y2 }, { x: x21, y: y21 }] = derivative2[0](t[1]) + return [ + (x - x1) * x11 + (y - y1) * y11, + (x - x2) * x21 + (y - y2) * y21, + (x - x1) ** 2 + (y - y1) ** 2 - r, + (x - x2) ** 2 + (y - y2) ** 2 - r, + ] + } + const f2 = (t: number[]): number[][] => { + const x = t[2], y = t[3] + const [{ x: x1, y: y1 }, { x: x11, y: y11 }, { x: x12, y: y12 }] = derivative1[1](t[0]) + const [{ x: x2, y: y2 }, { x: x21, y: y21 }, { x: x22, y: y22 }] = derivative2[1](t[1]) + // dz1/dt1 = -x1' x1' + (x - x1)x1'' - y1'y1' + (y - y1)y1'' + // dz1/dt2 = 0 + // dz1/dx = x1' + // dz1/dy = y1' + // dz2/dt1 = 0 + // dz2/dt2 = -x2'x2' + (x - x2)x2'' + - y2'y2' + (y - y2)y2'' + // dz2/dx = x2' + // dz2/dy = y2' + // dz3/dt1 = 2 x1'(x1 - x) + 2 y1'(y1 - y) + // dz3/dt2 = 0 + // dz3/dx = 2(x - x1) + // dz3/dy = 2(y - y1) + // dz4/dt1 = 0 + // dz4/dt2 = 2 x2'(x2 - x) + 2 y2'(y2 - y) + // dz4/dx = 2(x - x2) + // dz4/dy = 2(y - y2) + return [ + [ + -x11 * x11 + (x - x1) * x12 - y11 * y11 + (y - y1) * y12, + 0, + x11, + y11, + ], + [ + 0, + -x21 * x21 + (x - x2) * x22 + - y21 * y21 + (y - y2) * y22, + x21, + y21, + ], + [ + 2 * x11 * (x1 - x) + 2 * y11 * (y1 - y), + 0, + 2 * (x - x1), + 2 * (y - y1), + ], + [ + 0, + 2 * x21 * (x2 - x) + 2 * y21 * (y2 - y), + 2 * (x - x2), + 2 * (y - y2), + ], + ] + } + const result = newtonIterates([t1, t2, center.x, center.y], f1, f2, delta2) + if (!result) return + return { x: result[2], y: result[3] } +}