Merge pull request #6955 from HaasJona/add/circleSegment
Add path.circleSegment() function
This commit is contained in:
commit
e8482185ff
6 changed files with 152 additions and 69 deletions
|
@ -2,6 +2,7 @@ Unreleased:
|
||||||
Added:
|
Added:
|
||||||
core:
|
core:
|
||||||
- The `Path.rotate()` method was added to the core API.
|
- The `Path.rotate()` method was added to the core API.
|
||||||
|
- The `Path.circleSegment()` method was added to the core API.
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
brian:
|
brian:
|
||||||
|
|
|
@ -89,11 +89,11 @@ export function draftCurvedWaistband({
|
||||||
)
|
)
|
||||||
points.centerNotch = new Path()
|
points.centerNotch = new Path()
|
||||||
.move(points.ex1Rotated)
|
.move(points.ex1Rotated)
|
||||||
.curve(points.ex1cFlippedRotated, points.ex2cFlippedRotated, points.ex2FlippedRotated)
|
.circleSegment(-(an + anExtra), points.center)
|
||||||
.shiftAlong(store.get('waistbandOverlap') / 2)
|
.shiftAlong(store.get('waistbandOverlap') / 2)
|
||||||
points.buttonNotch = new Path()
|
points.buttonNotch = new Path()
|
||||||
.move(points.ex2Rotated)
|
.move(points.ex2Rotated)
|
||||||
.curve(points.ex2cRotated, points.ex1cRotated, points.ex1Rotated)
|
.circleSegment(an + anExtra, points.center)
|
||||||
.shiftAlong(store.get('waistbandOverlap'))
|
.shiftAlong(store.get('waistbandOverlap'))
|
||||||
macro('sprinkle', {
|
macro('sprinkle', {
|
||||||
snippet: 'notch',
|
snippet: 'notch',
|
||||||
|
|
52
markdown/dev/reference/api/path/circlesegment/en.md
Normal file
52
markdown/dev/reference/api/path/circlesegment/en.md
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
title: Path.circleSegment()
|
||||||
|
---
|
||||||
|
|
||||||
|
The `Path.circleSegment()` method draws a circle segment
|
||||||
|
starting from the current endpoint of the path around the given origin with a given angle.
|
||||||
|
|
||||||
|
A positive angle results in a counter-clockwise arc.
|
||||||
|
|
||||||
|
A negative angle results in a clockwise arc.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
The new endpoint of this path is the same point
|
||||||
|
that
|
||||||
|
```js
|
||||||
|
path.end().rotate(deg, origin)
|
||||||
|
```
|
||||||
|
would return.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
## Signature
|
||||||
|
|
||||||
|
```js
|
||||||
|
Path path.circleSegment(deg, origin)
|
||||||
|
```
|
||||||
|
|
||||||
|
<Tip compact>This method is chainable as it returns the `Path` object</Tip>
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<Example caption="Example of the Path.circleSegment() method">
|
||||||
|
```js
|
||||||
|
({ Point, points, Path, paths, part }) => {
|
||||||
|
|
||||||
|
points.from = new Point(10, 20)
|
||||||
|
points.origin = new Point(40, 0)
|
||||||
|
|
||||||
|
paths.line = new Path()
|
||||||
|
.move(points.from)
|
||||||
|
.circleSegment(90, points.origin)
|
||||||
|
.setText("→ Path.circleSegment() →", "text-sm center fill-note")
|
||||||
|
|
||||||
|
paths.helper = new Path()
|
||||||
|
.move(paths.line.start())
|
||||||
|
.line(points.origin)
|
||||||
|
.line(paths.line.end())
|
||||||
|
.setClass('dotted stroke-sm')
|
||||||
|
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Example>
|
|
@ -2,6 +2,7 @@ import { Attributes } from './attributes.mjs'
|
||||||
import { Point } from './point.mjs'
|
import { Point } from './point.mjs'
|
||||||
import { Bezier } from 'bezier-js'
|
import { Bezier } from 'bezier-js'
|
||||||
import {
|
import {
|
||||||
|
deg2rad,
|
||||||
linesIntersect,
|
linesIntersect,
|
||||||
lineIntersectsCurve,
|
lineIntersectsCurve,
|
||||||
curvesIntersect,
|
curvesIntersect,
|
||||||
|
@ -200,6 +201,43 @@ Path.prototype.bbox = function () {
|
||||||
return __bbbbox(bbs)
|
return __bbbbox(bbs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a circle section to this path.
|
||||||
|
* Positive angles create a counter-clockwise arc starting from the current point,
|
||||||
|
* negative angles create a clockwise arc.
|
||||||
|
*
|
||||||
|
* Note: This is unrelated to SVG arc segments in paths, we approximate the circle segment using
|
||||||
|
* standard cubic Bézier curves
|
||||||
|
*
|
||||||
|
* @param {number} deg span of the new circle section in degrees.
|
||||||
|
* @param {Point} origin center point of the circle (rotation origin)
|
||||||
|
* @returns {Path} this
|
||||||
|
*/
|
||||||
|
Path.prototype.circleSegment = function (deg, origin) {
|
||||||
|
const radius = this.end().dist(origin)
|
||||||
|
// ensure a full circle gets 4 segments for a good approximation
|
||||||
|
// We could use more, but this is not necessary
|
||||||
|
const steps = Math.ceil(Math.abs(deg / 90))
|
||||||
|
const stepAngle = deg / steps
|
||||||
|
const stepAngleRad = deg2rad(stepAngle)
|
||||||
|
|
||||||
|
// magic formula, calculate distance of control points for a good circle segment approximation
|
||||||
|
const distance = radius * (4.0 / 3.0) * Math.tan(stepAngleRad / 4)
|
||||||
|
|
||||||
|
// Append individual segments for arc approximation
|
||||||
|
// steps is usually between 1 and 4
|
||||||
|
for (let i = 0; i < steps; i++) {
|
||||||
|
const startPoint = this.end()
|
||||||
|
const endPoint = startPoint.rotate(stepAngle, origin)
|
||||||
|
const startAngle = startPoint.angle(origin) - 90
|
||||||
|
const endAngle = endPoint.angle(origin) + 90
|
||||||
|
const cp1 = startPoint.shift(startAngle, distance)
|
||||||
|
const cp2 = endPoint.shift(endAngle, distance)
|
||||||
|
this.curve(cp1, cp2, endPoint)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns this after cleaning out in-place path operations
|
* Returns this after cleaning out in-place path operations
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,6 +3,60 @@ import { round, Path, Point } from '../src/index.mjs'
|
||||||
import { pathsProxy } from '../src/path.mjs'
|
import { pathsProxy } from '../src/path.mjs'
|
||||||
|
|
||||||
describe('Path', () => {
|
describe('Path', () => {
|
||||||
|
describe('circleSegment', () => {
|
||||||
|
it('Should draw a circleSegment', () => {
|
||||||
|
const points = {}
|
||||||
|
points.origin = new Point(10, 20)
|
||||||
|
// radius = 100
|
||||||
|
points.start = points.origin.shift(77, 100)
|
||||||
|
|
||||||
|
const test = new Path().move(points.start).circleSegment(90, points.origin)
|
||||||
|
|
||||||
|
const endPoint = test.end()
|
||||||
|
const expectedEndPoint = points.start.rotate(90, points.origin)
|
||||||
|
|
||||||
|
expect(round(endPoint.x)).to.equal(round(expectedEndPoint.x))
|
||||||
|
expect(round(endPoint.y)).to.equal(round(expectedEndPoint.y))
|
||||||
|
expect(endPoint.sitsOn(expectedEndPoint)).to.equal(true)
|
||||||
|
expect(Math.round(test.length())).to.equal(Math.round(Math.PI * 50))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should draw a circleSegment with negative angle', () => {
|
||||||
|
const points = {}
|
||||||
|
points.origin = new Point(10, 20)
|
||||||
|
// radius = 100
|
||||||
|
points.start = points.origin.shift(-122, 100)
|
||||||
|
|
||||||
|
const test = new Path().move(points.start).circleSegment(-45, points.origin)
|
||||||
|
|
||||||
|
const endPoint = test.end()
|
||||||
|
const expectedEndPoint = points.start.rotate(-45, points.origin)
|
||||||
|
|
||||||
|
expect(round(endPoint.x)).to.equal(round(expectedEndPoint.x))
|
||||||
|
expect(round(endPoint.y)).to.equal(round(expectedEndPoint.y))
|
||||||
|
expect(endPoint.sitsOn(expectedEndPoint)).to.equal(true)
|
||||||
|
expect(Math.round(test.length())).to.equal(Math.round(Math.PI * 25))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should draw a full circle', () => {
|
||||||
|
const points = {}
|
||||||
|
|
||||||
|
points.origin = new Point(0, 0)
|
||||||
|
// radius = 100
|
||||||
|
points.start = points.origin.shift(0, 100)
|
||||||
|
|
||||||
|
const test = new Path().move(points.start).circleSegment(360, points.origin)
|
||||||
|
|
||||||
|
const endPoint = test.end()
|
||||||
|
|
||||||
|
expect(round(endPoint.x)).to.equal(round(points.start.x))
|
||||||
|
expect(round(endPoint.y)).to.equal(round(points.start.y))
|
||||||
|
expect(endPoint.sitsOn(points.start)).to.equal(true)
|
||||||
|
expect(Math.round(test.length())).to.equal(Math.round(Math.PI * 200))
|
||||||
|
expect(test.ops.length).to.equal(5) // 1 move + 4 cubic curves
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('smurve', () => {
|
describe('smurve', () => {
|
||||||
it('Should draw a smurve', () => {
|
it('Should draw a smurve', () => {
|
||||||
const points = {}
|
const points = {}
|
||||||
|
|
|
@ -10,35 +10,10 @@ export const getIds = (keys, id) => {
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Helper method to calculate the arc
|
|
||||||
*/
|
|
||||||
const roundExtended = (radius, angle = 90, utils) => {
|
|
||||||
const arg = utils.deg2rad(angle / 2)
|
|
||||||
|
|
||||||
return (radius * 4 * (1 - Math.cos(arg))) / Math.sin(arg) / 3
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Short IDs
|
* Short IDs
|
||||||
*/
|
*/
|
||||||
const keys = [
|
const keys = ['center', 'in1', 'in2', 'ex1', 'ex2', 'in2Flipped', 'ex2Flipped']
|
||||||
'center',
|
|
||||||
'in1',
|
|
||||||
'in1c',
|
|
||||||
'in2',
|
|
||||||
'in2c',
|
|
||||||
'ex1',
|
|
||||||
'ex1c',
|
|
||||||
'ex2',
|
|
||||||
'ex2c',
|
|
||||||
'in2Flipped',
|
|
||||||
'in2cFlipped',
|
|
||||||
'in1cFlipped',
|
|
||||||
'ex1cFlipped',
|
|
||||||
'ex2cFlipped',
|
|
||||||
'ex2Flipped',
|
|
||||||
]
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The plugin object itself
|
* The plugin object itself
|
||||||
|
@ -52,7 +27,7 @@ export const plugin = {
|
||||||
for (const id of Object.values(store.get([...storeRoot, 'paths']))) delete paths[id]
|
for (const id of Object.values(store.get([...storeRoot, 'paths']))) delete paths[id]
|
||||||
for (const id of Object.values(store.get([...storeRoot, 'points']))) delete points[id]
|
for (const id of Object.values(store.get([...storeRoot, 'points']))) delete points[id]
|
||||||
},
|
},
|
||||||
ringsector: function (mc, { utils, Point, points, Path, paths, store }) {
|
ringsector: function (mc, { Point, points, Path, paths, store }) {
|
||||||
const {
|
const {
|
||||||
angle,
|
angle,
|
||||||
insideRadius,
|
insideRadius,
|
||||||
|
@ -68,64 +43,30 @@ export const plugin = {
|
||||||
const ids = getIds(keys, id)
|
const ids = getIds(keys, id)
|
||||||
const pathIds = getIds(['path'], id)
|
const pathIds = getIds(['path'], id)
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the distance of the control point for the internal
|
|
||||||
* and external arcs using bezierCircleExtended
|
|
||||||
*/
|
|
||||||
const distIn = roundExtended(insideRadius, angle / 2, utils)
|
|
||||||
const distEx = roundExtended(outsideRadius, angle / 2, utils)
|
|
||||||
// The centre of the circles
|
// The centre of the circles
|
||||||
points[ids.center] = center.copy()
|
points[ids.center] = center.copy()
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is expected to draft ring sectors for
|
|
||||||
* angles up to 180%. Since roundExtended works
|
|
||||||
* best for angles until 90º, we generate the ring
|
|
||||||
* sector using the half angle and then duplicate it
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The first point of the internal arc, situated at
|
* The first point of the internal arc, situated at
|
||||||
* a insideRadius distance below the centre
|
* a insideRadius distance below the centre
|
||||||
*/
|
*/
|
||||||
points[ids.in1] = points[ids.center].shift(-90, insideRadius)
|
points[ids.in1] = points[ids.center].shift(-90, insideRadius)
|
||||||
|
|
||||||
/**
|
|
||||||
* The control point for 'in1'. It's situated at a
|
|
||||||
* distance $distIn calculated with bezierCircleExtended
|
|
||||||
* and the line between it and 'in1' is perpendicular to
|
|
||||||
* the line between 'in1' and the centre, so it's
|
|
||||||
* shifted in the direction 0º
|
|
||||||
*/
|
|
||||||
points[ids.in1c] = points[ids.in1].shift(0, distIn)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The second point of the internal arc, situated at
|
* The second point of the internal arc, situated at
|
||||||
* a $insideRadius distance of the centre in the direction
|
* a $insideRadius distance of the centre in the direction
|
||||||
* $angle/2 - 90º
|
* $angle/2 - 90º
|
||||||
*/
|
*/
|
||||||
points[ids.in2] = points[ids.center].shift(angle / 2 - 90, insideRadius)
|
points[ids.in2] = points[ids.center].shift(angle / 2 - 90, insideRadius)
|
||||||
|
|
||||||
/**
|
|
||||||
* The control point for 'in2'. It's situated at a
|
|
||||||
* distance $distIn calculated with bezierCircleExtended
|
|
||||||
* and the line between it and 'in2' is perpendicular to
|
|
||||||
* the line between 'in2' and the centre, so it's
|
|
||||||
* shifted in the direction $angle/2 + 180º
|
|
||||||
*/
|
|
||||||
points[ids.in2c] = points[ids.in2].shift(angle / 2 + 180, distIn)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The points for the external arc are generated in the
|
* The points for the external arc are generated in the
|
||||||
* same way, using $outsideRadius and $distEx instead
|
* same way, using $outsideRadius and $distEx instead
|
||||||
*/
|
*/
|
||||||
points[ids.ex1] = points[ids.center].shift(-90, outsideRadius)
|
points[ids.ex1] = points[ids.center].shift(-90, outsideRadius)
|
||||||
points[ids.ex1c] = points[ids.ex1].shift(0, distEx)
|
|
||||||
points[ids.ex2] = points[ids.center].shift(angle / 2 - 90, outsideRadius)
|
points[ids.ex2] = points[ids.center].shift(angle / 2 - 90, outsideRadius)
|
||||||
points[ids.ex2c] = points[ids.ex2].shift(angle / 2 + 180, distEx)
|
|
||||||
|
|
||||||
// Flip all the points to generate the full ring sector
|
// Flip all the points to generate the full ring sector
|
||||||
for (const id of ['in2', 'in2c', 'in1c', 'ex1c', 'ex2c', 'ex2']) {
|
for (const id of ['in2', 'ex2']) {
|
||||||
points[ids[id + 'Flipped']] = points[ids[id]].flipX(center)
|
points[ids[id + 'Flipped']] = points[ids[id]].flipX(center)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,12 +89,9 @@ export const plugin = {
|
||||||
// Construct the path of the full ring sector
|
// Construct the path of the full ring sector
|
||||||
paths[pathIds.path] = new Path()
|
paths[pathIds.path] = new Path()
|
||||||
.move(points[ids.ex2Flipped])
|
.move(points[ids.ex2Flipped])
|
||||||
.curve(points[ids.ex2cFlipped], points[ids.ex1cFlipped], points[ids.ex1])
|
.circleSegment(angle, points[ids.center])
|
||||||
.curve(points[ids.ex1c], points[ids.ex2c], points[ids.ex2])
|
|
||||||
.line(points[ids.in2])
|
.line(points[ids.in2])
|
||||||
.curve(points[ids.in2c], points[ids.in1c], points[ids.in1])
|
.circleSegment(-angle, points[ids.center])
|
||||||
.curve(points[ids.in1cFlipped], points[ids.in2cFlipped], points[ids.in2Flipped])
|
|
||||||
.line(points[ids.ex2Flipped])
|
|
||||||
.close()
|
.close()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue