Skip to content

Commit 85e027e

Browse files
split a PathComponent based on inner-curves or outer-curves bias
1 parent 796e118 commit 85e027e

File tree

2 files changed

+154
-34
lines changed

2 files changed

+154
-34
lines changed

BezierKit/Library/PathComponent.swift

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -523,67 +523,128 @@ import Foundation
523523
}
524524
}
525525

526-
open func split(standardizedRange range: PathComponentRange) -> Self {
526+
open func split(standardizedRange range: PathComponentRange, bias: PathComponentBias) -> Self {
527527
assert(range.isStandardized)
528-
529528
guard !self.isPoint else { return self }
530529

531-
let start = range.start
532-
let end = range.end
533-
534-
var resultPoints: [CGPoint] = []
535-
var resultOrders: [Int] = []
536-
537-
func appendElement(_ index: Int, _ start: CGFloat, _ end: CGFloat, includeStart: Bool, includeEnd: Bool) {
530+
func splitElement(at index: Int, start: CGFloat, end: CGFloat, includeStart: Bool, includeEnd: Bool) -> (points: [CGPoint], order: Int) {
538531
assert(includeStart || includeEnd)
539-
let element = self.element(at: index).split(from: start, to: end)
532+
let element = element(at: index).split(from: start, to: end)
540533
let startIndex = includeStart ? 0 : 1
541534
let endIndex = includeEnd ? element.order : element.order - 1
542-
resultPoints += element.points[startIndex...endIndex]
543-
resultOrders.append(self.orders[index])
544-
}
545-
546-
if start.elementIndex == end.elementIndex {
547-
// we just need to go from start.t to end.t
548-
appendElement(start.elementIndex, start.t, end.t, includeStart: true, includeEnd: true)
549-
} else {
535+
return (
536+
points: Array(element.points[startIndex...endIndex]),
537+
order: orders[index]
538+
)
539+
}
540+
541+
func splitInner(start: IndexedPathComponentLocation, end: IndexedPathComponentLocation) -> (points: [CGPoint], orders: [Int]) {
542+
guard start.elementIndex != end.elementIndex else {
543+
// we just need to go from start.t to end.t
544+
let (points, order) = splitElement(at: start.elementIndex, start: start.t, end: end.t, includeStart: true, includeEnd: true)
545+
return (points, [order])
546+
}
547+
var resultPoints: [CGPoint] = []
548+
var resultOrders: [Int] = []
549+
550550
// if end.t = 1, append from start.elementIndex+1 through end.elementIndex, otherwise to end.elementIndex
551-
let lastFullElementIndex = end.t != 1.0 ? (end.elementIndex-1) : end.elementIndex
552-
let firstFullElementIndex = start.t != 0.0 ? (start.elementIndex+1) : start.elementIndex
551+
let lastElementIndex = end.t != 1.0 ? (end.elementIndex-1) : end.elementIndex
552+
let firstElementIndex = start.t != 0.0 ? (start.elementIndex+1) : start.elementIndex
553+
553554
// if needed, append start.elementIndex from t=start.t to t=1
554-
if firstFullElementIndex != start.elementIndex {
555-
appendElement(start.elementIndex, start.t, 1.0, includeStart: true, includeEnd: false)
555+
if firstElementIndex != start.elementIndex {
556+
let (points, order) = splitElement(at: start.elementIndex, start: start.t, end: 1.0, includeStart: true, includeEnd: false)
557+
resultPoints.append(contentsOf: points)
558+
resultOrders.append(order)
556559
}
557560
// if there exist full elements to copy, use the fast path to get them all in one fell swoop
558-
let hasFullElements = firstFullElementIndex <= lastFullElementIndex
561+
let hasFullElements = firstElementIndex <= lastElementIndex
559562
if hasFullElements {
560-
resultPoints += self.points[self.offsets[firstFullElementIndex] ... self.offsets[lastFullElementIndex] + self.orders[lastFullElementIndex]]
561-
resultOrders += self.orders[firstFullElementIndex ... lastFullElementIndex]
563+
let points = points[offsets[firstElementIndex] ... offsets[lastElementIndex] + orders[lastElementIndex]]
564+
let orders = orders[firstElementIndex ... lastElementIndex]
565+
resultPoints.append(contentsOf: points)
566+
resultOrders.append(contentsOf: orders)
562567
}
563568
// if needed, append from end.elementIndex from t=0, to t=end.t
564-
if lastFullElementIndex != end.elementIndex {
565-
appendElement(end.elementIndex, 0.0, end.t, includeStart: !hasFullElements, includeEnd: true)
569+
if lastElementIndex != end.elementIndex {
570+
let (points, order) = splitElement(at: end.elementIndex, start: 0.0, end: end.t, includeStart: !hasFullElements, includeEnd: true)
571+
resultPoints.append(contentsOf: points)
572+
resultOrders.append(order)
573+
}
574+
return (points: resultPoints, orders: resultOrders)
575+
}
576+
577+
func splitOuter(start: IndexedPathComponentLocation, end: IndexedPathComponentLocation) -> (points: [CGPoint], orders: [Int]) {
578+
var resultPoints: [CGPoint] = []
579+
var resultOrders: [Int] = []
580+
581+
// if end.t = 0, append from end.elementIndex+1 through start.elementIndex-1, otherwise from end.elementIndex
582+
let lastElementIndex = end.t != 0.0 ? (end.elementIndex+1) : end.elementIndex
583+
let firstElementIndex = start.t != 1.0 ? (start.elementIndex-1) : start.elementIndex
584+
585+
// if there exist full elements to copy, use the fast path to get them all in one fell swoop
586+
let hasFullElementsL = firstElementIndex > 0
587+
let hasFullElementsR = lastElementIndex < numberOfElements
588+
589+
if lastElementIndex != end.elementIndex { // right splitted curve
590+
let includeEndPoint = !(hasFullElementsR || hasFullElementsL)
591+
let (points, order) = splitElement(at: end.elementIndex, start: end.t, end: 1.0, includeStart: true, includeEnd: includeEndPoint)
592+
resultPoints.append(contentsOf: points)
593+
resultOrders.append(order)
566594
}
595+
if hasFullElementsR {
596+
let points = points[offsets[lastElementIndex] ... offsets[numberOfElements - 1] + orders[numberOfElements - 1]]
597+
let orders = orders[lastElementIndex ..< numberOfElements]
598+
resultPoints.append(contentsOf: points)
599+
resultOrders.append(contentsOf: orders)
600+
}
601+
if hasFullElementsL {
602+
let points = points[0 ..< offsets[firstElementIndex] + orders[firstElementIndex] + (hasFullElementsR ? 0 : 1)]
603+
let orders = orders[0 ... firstElementIndex]
604+
resultPoints.append(contentsOf: points)
605+
resultOrders.append(contentsOf: orders)
606+
}
607+
if firstElementIndex != start.elementIndex { // left splitted curve
608+
let (points, order) = splitElement(at: start.elementIndex, start: 0.0, end: start.t, includeStart: false, includeEnd: true)
609+
resultPoints.append(contentsOf: points)
610+
resultOrders.append(order)
611+
}
612+
return (points: resultPoints, orders: resultOrders)
613+
}
614+
615+
switch bias {
616+
case .inner:
617+
let (resultPoints, resultOrders) = splitInner(
618+
start: range.start,
619+
end: range.end
620+
)
621+
return type(of: self).init(points: resultPoints, orders: resultOrders)
622+
623+
case .outer:
624+
let (resultPoints, resultOrders) = splitOuter(
625+
start: range.start,
626+
end: range.end
627+
)
628+
return type(of: self).init(points: resultPoints, orders: resultOrders)
567629
}
568-
return type(of: self).init(points: resultPoints, orders: resultOrders)
569630
}
570631

571-
public func split(range: PathComponentRange) -> Self {
632+
public func split(range: PathComponentRange, bias: PathComponentBias) -> Self {
572633
let reverse = range.end < range.start
573-
let result = self.split(standardizedRange: range.standardized)
634+
let result = self.split(standardizedRange: range.standardized, bias: bias)
574635
return reverse ? result.reversed() : result
575636
}
576637

577-
public func split(from start: IndexedPathComponentLocation, to end: IndexedPathComponentLocation) -> Self {
578-
return self.split(range: PathComponentRange(from: start, to: end))
638+
public func split(from start: IndexedPathComponentLocation, to end: IndexedPathComponentLocation, bias: PathComponentBias = .inner) -> Self {
639+
return self.split(range: PathComponentRange(from: start, to: end), bias: bias)
579640
}
580641

581642
open func reversed() -> Self {
582643
return type(of: self).init(points: self.points.reversed(), orders: self.orders.reversed())
583644
}
584645

585646
open func copy(using t: CGAffineTransform) -> Self {
586-
return type(of: self).init(points: self.points.map { $0.applying(t) }, orders: self.orders )
647+
return type(of: self).init(points: self.points.map { $0.applying(t) }, orders: self.orders)
587648
}
588649
}
589650

@@ -642,3 +703,8 @@ public struct PathComponentRange: Equatable {
642703
return PathComponentRange(from: start, to: end)
643704
}
644705
}
706+
707+
public enum PathComponentBias: Equatable {
708+
case inner
709+
case outer
710+
}

BezierKit/MacDemos/Demos.swift

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,60 @@ class Demos {
466466
}
467467

468468
})
469+
470+
static let demo23 = Demo(title: "PathComponent split.inner",
471+
quadraticControlPoints: [],
472+
cubicControlPoints: [],
473+
drawFunction: {(context: CGContext, demoState: DemoState) in
474+
475+
Draw.reset(context)
476+
477+
var flip = CGAffineTransform(scaleX: 1, y: -1)
478+
let font = CTFontCreateWithName("Times" as CFString, 350, &flip)
479+
let height = CTFontGetXHeight(font)
480+
var translate = CGAffineTransform.init(translationX: 0, y: -height + 15)
481+
482+
var unichar: UniChar = ("H" as NSString).character(at: 0)
483+
var glyph: CGGlyph = 0
484+
CTFontGetGlyphsForCharacters(font, &unichar, &glyph, 1)
485+
486+
let cgPath: CGPath = CTFontCreatePathForGlyph(font, glyph, nil)!
487+
let path = Path(cgPath: cgPath.copy(using: &translate)!)
488+
489+
let component = path.components[0]
490+
let splitted1 = component.split(from: .init(elementIndex: 0, t: 0), to: .init(elementIndex: component.numberOfElements/2, t: 1), bias: .inner)
491+
let splitted2 = component.split(from: .init(elementIndex: component.numberOfElements/2, t: 1), to: .init(elementIndex: 0, t: 0), bias: .inner)
492+
assert(splitted1 == splitted2.reversed())
493+
494+
Draw.drawPath(context, Path(components: [splitted2]))
495+
})
496+
497+
static let demo24 = Demo(title: "PathComponent split.outer",
498+
quadraticControlPoints: [],
499+
cubicControlPoints: [],
500+
drawFunction: {(context: CGContext, demoState: DemoState) in
501+
502+
Draw.reset(context)
503+
504+
var flip = CGAffineTransform(scaleX: 1, y: -1)
505+
let font = CTFontCreateWithName("Times" as CFString, 350, &flip)
506+
let height = CTFontGetXHeight(font)
507+
var translate = CGAffineTransform.init(translationX: 0, y: -height + 15)
508+
509+
var unichar: UniChar = ("H" as NSString).character(at: 0)
510+
var glyph: CGGlyph = 0
511+
CTFontGetGlyphsForCharacters(font, &unichar, &glyph, 1)
512+
513+
let cgPath: CGPath = CTFontCreatePathForGlyph(font, glyph, nil)!
514+
let path = Path(cgPath: cgPath.copy(using: &translate)!)
515+
516+
let component = path.components[0]
517+
let splitted1 = component.split(from: .init(elementIndex: 0, t: 0), to: .init(elementIndex: component.numberOfElements/2, t: 1), bias: .outer)
518+
let splitted2 = component.split(from: .init(elementIndex: component.numberOfElements/2, t: 1), to: .init(elementIndex: 0, t: 0), bias: .outer)
519+
assert(splitted1 == splitted2.reversed())
520+
521+
Draw.drawPath(context, Path(components: [splitted1]))
522+
})
469523

470-
static let all: [Demo] = [demo1, demo2, demo3, demo4, demo5, demo6, demo7, demo8, demo9, demo10, demo11, demo12, demo13, demo14, demo15, demo16, demo17, demo18, demo19, demo20, demo21, demo22]
524+
static let all: [Demo] = [demo1, demo2, demo3, demo4, demo5, demo6, demo7, demo8, demo9, demo10, demo11, demo12, demo13, demo14, demo15, demo16, demo17, demo18, demo19, demo20, demo21, demo22, demo23, demo24]
471525
}

0 commit comments

Comments
 (0)