From a9a0556b178b216d523158b51cb8571044341fd0 Mon Sep 17 00:00:00 2001 From: Belleve Date: Wed, 31 Jul 2024 01:55:49 -1000 Subject: [PATCH] Add fraktur E and G (#2448) * Add - MATHEMATICAL FRAKTUR CAPITAL E (`U+1D508`) (#444). - MATHEMATICAL FRAKTUR CAPITAL G (`U+1D50A`) (#444). * Cleanup * Refine --- changes/31.1.0.md | 2 + packages/font-glyphs/src/common/shapes.ptl | 60 +++-- .../font-glyphs/src/letter-like/fraktur.ptl | 189 +++++++++----- packages/font-glyphs/src/meta/macros.ptl | 6 +- .../font-kits/src/derived-coordinates.mjs | 28 +- packages/font-kits/src/spiro-kit.mjs | 73 +++--- packages/geometry-cache/src/index.mjs | 2 +- packages/geometry/package.json | 4 +- packages/geometry/src/index.mjs | 20 +- packages/geometry/src/spiro-control.mjs | 161 ++---------- packages/geometry/src/spiro-expand.mjs | 176 ++++++++++++- packages/geometry/src/spiro-pen-expand.mjs | 243 ++++++++++++++++++ packages/geometry/src/spiro-pen-expander.mjs | 81 ------ packages/geometry/src/spiro-to-outline.mjs | 32 +++ 14 files changed, 716 insertions(+), 361 deletions(-) create mode 100644 packages/geometry/src/spiro-pen-expand.mjs delete mode 100644 packages/geometry/src/spiro-pen-expander.mjs diff --git a/changes/31.1.0.md b/changes/31.1.0.md index b78655f51..5238a06da 100644 --- a/changes/31.1.0.md +++ b/changes/31.1.0.md @@ -9,6 +9,8 @@ - LATIN SMALL LETTER BLACKLETTER E (`U+AB32`) (#2443). - LATIN SMALL LETTER BLACKLETTER O (`U+AB3D`) (#2443). - LATIN SMALL LETTER BLACKLETTER O WITH STROKE (`U+AB3E`) (#2443). + - MATHEMATICAL FRAKTUR CAPITAL E (`U+1D508`) (#444). + - MATHEMATICAL FRAKTUR CAPITAL G (`U+1D50A`) (#444). - MATHEMATICAL FRAKTUR CAPITAL J (`U+1D50D`) (#444). - MATHEMATICAL FRAKTUR SMALL E (`U+1D522`) (#444). - MATHEMATICAL FRAKTUR SMALL O (`U+1D52C`) (#444). diff --git a/packages/font-glyphs/src/common/shapes.ptl b/packages/font-glyphs/src/common/shapes.ptl index c5f192796..59cc4dfc4 100644 --- a/packages/font-glyphs/src/common/shapes.ptl +++ b/packages/font-glyphs/src/common/shapes.ptl @@ -4,7 +4,7 @@ import [mix linreg clamp fallback boole boolePn] from "@iosevka/util" import [Transform] from "@iosevka/geometry/transform" import [Interpolator WithKnotProxy] from "@iosevka/geometry/spiro-control" import [RadicalGeometry StrokeGeometry RemoveHolesGeometry] from "@iosevka/geometry" -import [CMixCoord] from "@iosevka/font-kits/derived-coordinates" +import [CMixCoord CopyBackKnotProxy] from "@iosevka/font-kits/derived-coordinates" glyph-module @@ -512,7 +512,7 @@ glyph-block CommonShapes : begin return : HookShape before after false args define [hookProxy args] : begin - return : g4 [new CMixCoord 0.5] args.y + return : new CopyBackKnotProxy [g4 [new CMixCoord 0.5] args.y] args 2 glyph-block-export hookstart define flex-params [hookstart] : begin @@ -566,17 +566,40 @@ glyph-block CommonShapes : begin if args.rtl g2.left.mid g2.right.mid return : knotType (args.x + italicAdj) (args.y + overshoot) af - define [mixR w v u sw] : begin - if (w < v) : return : 1 - [mixR v w u sw] - local superness DesignParameters.superness - local r : 1 / ([Math.pow (1 - [Math.pow (1 - v / w) superness]) (1 / superness)] + 1) - set r : 0.5 + (r - 0.5) * (v + w) / (u * 2) - return r + define [superSin angle superness] : begin + if [not angle] : return 0 + local s : Math.sin (angle * Math.PI / 180) + local sign : if (s < 0) (-1) 1 + return : sign * [Math.pow [Math.abs s] (2 / superness)] + + define [superCos angle superness] : begin + if [not angle] : return 1 + local c : Math.cos (angle * Math.PI / 180) + local sign : if (c < 0) (-1) 1 + return : sign * [Math.pow [Math.abs c] (2 / superness)] + + define [mixR w angW v angV u] : begin + if (w < v) : return : 1 - [mixR v angV w angW u] + + local super DesignParameters.superness + + if (angV != nothing && angW != nothing) + : then : begin + local radV : v / (1 - [superSin angV super]) + local radW : w / (1 - [superSin angW super]) + local chrV : radV * [superCos angV super] + local chrW : radW * [superCos angW super] + return : chrW / (chrV + chrW) + : else : begin + local r : 1 / ([Math.pow (1 - [Math.pow (1 - v / w) super]) (1 / super)] + 1) + set r : 0.5 + (r - 0.5) * (v + w) / (u * 2) + return r define [archBlenderProxy args] : begin - return : [if args.compact g2 g4] + local p : [if args.compact g2 g4] new CMixCoord [fallback args.p 0.5] 0 args.mockPre args.mockPost begin args.y + return : new CopyBackKnotProxy p args 2 define [archBlender _before _after args] : begin local before : fallback args.mockPre _before @@ -585,8 +608,7 @@ glyph-block CommonShapes : begin local u : Math.abs (after.x - before.x) local v : Math.abs (after.y - args.y) local w : Math.abs (before.y - args.y) - - local mixRatio : mixR w v u args.sw + local mixRatio : mixR w args.anglePre v args.anglePost u set args.x : mix before.x after.x [fallback args.p mixRatio] set args.rtl : before.x > after.x @@ -603,13 +625,15 @@ glyph-block CommonShapes : begin local-parameter : sw -- Stroke local-parameter : compact -- false local-parameter : o -- O + local-parameter : anglePre -- nothing + local-parameter : anglePost -- nothing local-parameter : swBefore -- sw local-parameter : swAfter -- sw local-parameter : mockPre -- nothing local-parameter : mockPost -- nothing - local-parameter : blendPre -- [arcvh] - local-parameter : blendPost -- [archv] - local args : object [lhs true] y p sw compact o swBefore swAfter mockPre mockPost blendPre blendPost + local-parameter : blendPre -- [if anglePre nothing [arcvh]] + local-parameter : blendPost -- [if anglePost nothing [archv]] + local args : object [lhs true] y p sw compact o swBefore swAfter mockPre mockPost blendPre blendPost anglePre anglePost return : WithKnotProxy [archBlenderProxy args] : Interpolator archBlender args export : define flex-params [rhs] : begin @@ -618,13 +642,15 @@ glyph-block CommonShapes : begin local-parameter : sw -- Stroke local-parameter : compact -- false local-parameter : o -- O + local-parameter : anglePre -- nothing + local-parameter : anglePost -- nothing local-parameter : swBefore -- sw local-parameter : swAfter -- sw local-parameter : mockPre -- nothing local-parameter : mockPost -- nothing - local-parameter : blendPre -- [arcvh] - local-parameter : blendPost -- [archv] - local args : object [lhs false] y p sw compact o swBefore swAfter mockPre mockPost blendPre blendPost + local-parameter : blendPre -- [if anglePre nothing [arcvh]] + local-parameter : blendPost -- [if anglePost nothing [archv]] + local args : object [lhs false] y p sw compact o swBefore swAfter mockPre mockPost blendPre blendPost anglePre anglePost return : WithKnotProxy [archBlenderProxy args] : Interpolator archBlender args foreach side {lhs rhs} : begin diff --git a/packages/font-glyphs/src/letter-like/fraktur.ptl b/packages/font-glyphs/src/letter-like/fraktur.ptl index e503b0c24..e7e8a8b5f 100644 --- a/packages/font-glyphs/src/letter-like/fraktur.ptl +++ b/packages/font-glyphs/src/letter-like/fraktur.ptl @@ -11,6 +11,7 @@ import [SpiroPenGeometry] from "@iosevka/geometry" import [Vec2] from "@iosevka/geometry/point" import [Box] from "@iosevka/geometry/box" import [Interpolator] from "@iosevka/geometry/spiro-control" +import [PenKnotCollector] from "@iosevka/geometry/spiro-pen-expand" glyph-module @@ -30,14 +31,23 @@ glyph-block LetterLike-Fraktur-Shared : begin this.knots = knots public [applyToGlyph glyph] : begin - local c : spiro-collect glyph this.knots - local geom : new SpiroPenGeometry - begin c.gizmo - begin c.collector.closed - this.profile.getPenShape c.gizmo - c.collector.controls.map : function [k] [k.toMono] - return : glyph.includeGeometry geom + local defaultProfile : this.profile.getPenShape glyph.gizmo + local collector : new PenKnotCollector glyph.gizmo defaultProfile + local c : spiro-collect collector this.knots + local geom : new SpiroPenGeometry + begin glyph.gizmo + begin defaultProfile + begin collector.closed + begin collector.knots + + glyph.includeGeometry geom + return collector.knots + + # Directive to change the profile + glyph-block-export change-profile + define [change-profile newProfile] : function : begin + this.setProfile : newProfile.getPenShape this.gizmo # A pen profile describes a virtual flat-tip pen. We use a 45-degree arrangement to # simplify the math. @@ -126,47 +136,88 @@ glyph-block LetterLike-Fraktur-Shared : begin glyph-block LetterLike-Fraktur : begin glyph-block-import Common-Derivatives glyph-block-import CommonShapes - glyph-block-import LetterLike-Fraktur-Shared : S F T fraktur-stroke + glyph-block-import LetterLike-Fraktur-Shared : S F T fraktur-stroke change-profile glyph-block-import LetterLike-Fraktur-Shared : DecoSizeX DecoSizeY SlopeA SlopeB glyph-block-import LetterLike-Fraktur-Shared : Wave LbFootRise FHook PHexTop PHexBot define [LtDecorativeWave sx sy ey] : fraktur-stroke F - g2.ld.start [F.connL S : S.xl sx] [F.connB S : S.yt sy] + g2.ld.start sx sy ~~~ [Wave.vc (-Wave.DepthX)] - g2.ld.end pre@ (ey - 2 * S.thick) + g2.ld.end pre@ (ey - 2 * S.thick) - create-glyph "frak/C" 0x212D : glyph-proc - include : MarkSet.capital + do "C, E, and G" + define flex-params [FrakCTopAndCenterStroke] : glyph-proc + local-parameter : top -- [S.yt CAP] + local-parameter : bot -- [S.yp 0 CAP 0.375] + local-parameter : left -- [S.xl SB] + local-parameter : right -- [S.xr RightSB] - local xCenter : [mix SB RightSB 0.5] + 0.75 * F.thick + local xCenter : [mix left right 0.5] + 0.25 * F.thick + local deco : Math.max DecoSizeX (0.5 * ArchDepthA - 2 * S.thick) + local waveTop : top - deco - # Top-right stroke - include : fraktur-stroke S - g2 [S.xr RightSB] [S.yt CAP] - g2.left.mid [mix (xCenter + DecoSizeX) [S.xr RightSB] 0.5] ([S.yt CAP] - 0.5 * FHook) - corner (xCenter + DecoSizeX) [S.yt CAP] - corner xCenter ([S.yt CAP] - DecoSizeX) + # Top-right stroke + include : fraktur-stroke S + g2 right top + g2.left.mid [mix@ 0.5] (top - 0.5 * FHook) + corner (post@ <+> deco) top + corner xCenter (top - deco) - # Left and bottom stroke - include : fraktur-stroke S - g4.ld.start [S.xp SB RightSB 0.1] ([S.yt CAP] - 0.1 * ArchDepthA) - flat [S.xl SB] ([S.yt CAP] - 0.6 * ArchDepthA) - curl pre@ ([S.yb 0] + ArchDepthB) - ~~~ [hookend [S.yb 0] (sw -- S.thick)] - g2 [S.xr RightSB] ([S.yb 0] + SHook) + # Inner decoration + include : fraktur-stroke F + g2.ld.start [F.connL S xCenter] [F.connB S : top - deco] + ~~~ [Wave.vc (-Wave.DepthX)] + g2.ld.end pre@ bot - # A thin connection between the two strokes - include : fraktur-stroke T - g4.ru.start [T.connR S : S.xp SB RightSB 0.1] [T.connT S : [S.yt CAP] - 0.1 * ArchDepthA] - ~~~ [arch.lhs [T.yt CAP] 0.6 (sw -- T.thick) (blendPre -- null) (blendPost -- null)] - g4.ld.end [T.connL S xCenter] [T.connB S : [S.yt CAP] - DecoSizeX] + return : object xCenter [waveTop : top - deco] [waveBot bot] - # Inner decoration - include : fraktur-stroke F - g2.ld.start [F.connL S xCenter] [F.connB S : [S.yt CAP] - DecoSizeX] - ~~~ [Wave.vc (-Wave.DepthX)] - g2.ld.end pre@ (CAP * 0.375) + define flex-params [FrakCOutlineShape] : glyph-proc + local-parameter : mode -- 'C' + local-parameter : top -- [S.yt CAP] + local-parameter : bot -- [S.yb 0] + local-parameter : left -- [S.xl SB] + local-parameter : right -- [S.xr RightSB] + + local [object xCenter waveTop waveBot] : include : FrakCTopAndCenterStroke + bot -- ([mix bot top 0.1] + 2 * Wave.DepthY + 0.5 * S.thick) + + # Left and bottom stroke + local mainStroke : include : fraktur-stroke S + g4.ru.start [T.connL S xCenter] [T.connB S waveTop] [change-profile T] + arch.rhs [T.connT S top] (sw -- T.thick) (anglePre -- (-45)) (anglePost -- 0) + flat left (pre@ <-> 0.6 * ArchDepthA) [change-profile S] + curl pre@ (post@ <+> ArchDepthB) + match mode + : [Just 'C'] : list # C and E + hookend (sw -- S.thick) bot + g2 [T.connR S right] (pre@ <+> [T.connT S SHook]) [change-profile T] + : [Just 'G'] : list # G + arch.lhs (sw -- S.thick) bot + flat right (pre@ <+> ArchDepthA) + decor@ : curl pre@ (post@ <-> 0.5 * ArchDepthB) + decor@@ : g2 [post@tang-out 1] [post@tang-out SlopeB] + decor@ : corner [mix@rev PHexTop] [post@slope SlopeA] + decor@ : corner xCenter [mix waveTop waveBot 0.5] + + return : object xCenter waveTop waveBot + + create-glyph "frak/C" 0x212D : glyph-proc + include : MarkSet.capital + include : FrakCOutlineShape (mode -- 'C') + + create-glyph "frak/E" 0x1D508 : glyph-proc + include [refer-glyph "frak/C"] AS_BASE ALSO_METRICS + local [object xCenter waveTop waveBot] : include : FrakCOutlineShape (mode -- 'C') + + include : fraktur-stroke F + corner [F.connL S xCenter] [F.connB S : mix waveTop waveBot 0.5] + corner (post@ <-> 0.5 * DecoSizeX) [pre@slope SlopeA] + corner [F.xr RightSB] (pre@ <-> 0.5 * DecoSizeY) + + create-glyph "frak/G" 0x1D50A : glyph-proc + include : MarkSet.capital + include : FrakCOutlineShape (mode -- 'G') create-glyph "frak/H" 0x210C : glyph-proc include : MarkSet.capDesc @@ -200,37 +251,39 @@ glyph-block LetterLike-Fraktur : begin g2.down.mid [S.xp xLeftStem RightSB 0.75] [S.yp Descender(XH - FHook) 0.25] g2 [S.xr RightSB] [S.yb Descender] - define [IJTopStroke] : glyph-proc - include : fraktur-stroke S - g2.ld.start [S.xr RightSB] [S.yt CAP] - ~~~ [Wave.h] - g2.ld.mid [S.xl SB] [S.yt (CAP - Wave.DepthY)] - include : LtDecorativeWave [S.xl SB] (CAP - Wave.DepthY) (CAP * 0.625) + do "I, J" + define [IJTopStroke] : glyph-proc + include : fraktur-stroke S + g2.ld.start [S.xr RightSB] [S.yt CAP] + ~~~ [Wave.h] + g2.ld.end [S.xl SB] (pre@ <-> Wave.DepthY) [change-profile F] + ~~~ [Wave.vc (-Wave.DepthX)] + g2.ld.end pre@ (CAP * 0.625 - 2 * F.thick) - create-glyph "frak/I" 0x2111 : glyph-proc - include : MarkSet.capital - include : IJTopStroke + create-glyph "frak/I" 0x2111 : glyph-proc + include : MarkSet.capital + include : IJTopStroke - local iBox : S.box CAP 0 SB RightSB - include : fraktur-stroke S - g2.ld.start iBox.right iBox.top - g4 [iBox.xp 0.7] [mix@ 0.375] - g4 iBox.right (post@ <+> ArchDepthA) - hookend (sw -- S.thick) iBox.bot - g2 iBox.left (pre@ <+> SHook) + local iBox : S.box CAP 0 SB RightSB + include : fraktur-stroke S + g2.ld.start iBox.right iBox.top + g4 [iBox.xp 0.7] [mix@ 0.375] + g4 iBox.right (post@ <+> ArchDepthA) + hookend (sw -- S.thick) iBox.bot + g2 iBox.left (pre@ <+> SHook) - create-glyph "frak/J" 0x1D50D : glyph-proc - include : MarkSet.capDesc - include : IJTopStroke + create-glyph "frak/J" 0x1D50D : glyph-proc + include : MarkSet.capDesc + include : IJTopStroke - local jBox : S.box [S.yt (CAP - 1.5 * Wave.DepthY - 2 * S.thick)] Descender SB RightSB - include : fraktur-stroke S - g4 jBox.right (post@ <-> FHook) - hookstart (sw -- S.thick) jBox.top - g4 ([jBox.xp 0.6] - S.thick) (pre@ <-> 0.6 * ArchDepthA) - g4 jBox.right (post@ <+> ArchDepthA) - hookend (sw -- S.thick) jBox.bot - g2 jBox.left (pre@ <+> SHook) + local jBox : S.box [S.yt (CAP - 1.5 * Wave.DepthY - 2 * S.thick)] Descender SB RightSB + include : fraktur-stroke S + g4 [T.connR S jBox.right] (post@ <-> FHook) [change-profile T] + hookstart (sw -- S.thick) [T.connT S jBox.top] + g4 ([jBox.xp 0.6] - S.thick) (pre@ <-> 0.6 * ArchDepthA) [change-profile S] + g4 jBox.right (post@ <+> ArchDepthA) + hookend (sw -- S.thick) jBox.bot + g2 jBox.left (pre@ <+> SHook) create-glyph "frak/R" 0x211C : glyph-proc include : MarkSet.capital @@ -242,7 +295,7 @@ glyph-block LetterLike-Fraktur : begin local adb : 0.6 * ArchDepthB # Deocration at top-left - include : LtDecorativeWave xExt (CAP - ltHook) (CAP * 0.625) + # include : LtDecorativeWave [S.xl xExt] [S.yt (CAP - Wave.DepthY)] (CAP * 0.625) local xMidStrokeL : S.xl xLeftStem local xMidStrokeR : S.xp xLeftStem RightSB 0.625 @@ -255,9 +308,11 @@ glyph-block LetterLike-Fraktur : begin local yArchStart : yArchTop - SlopeA * (xArchTop - xArchStart) # Left stroke - include : fraktur-stroke S - g2.ru.start [S.xl xExt] [S.yt (CAP - ltHook)] - ~~~ [arch.rhs [S.yt CAP] 0.6 (blendPre -- null)] + include : fraktur-stroke F + g2.ru.start post@ (CAP * 0.625 - 2 * F.thick) + ~~~ [Wave.vc Wave.DepthX] + g2.ru.start [S.xl xExt] (post@ <-> ltHook) [change-profile S] + arch.rhs (blendPre -- null) [S.yt CAP] 0.625 flat [S.xl xLeftStem] [Math.max ([S.yb CAP] - ada) yArchStart] curl (pre@ <+> 0) yMidStrokeL corner [S.xl xExt] (post@ <+> LbFootRise) diff --git a/packages/font-glyphs/src/meta/macros.ptl b/packages/font-glyphs/src/meta/macros.ptl index ed0e02526..b44c1c817 100644 --- a/packages/font-glyphs/src/meta/macros.ptl +++ b/packages/font-glyphs/src/meta/macros.ptl @@ -288,13 +288,13 @@ define-macro glyph-block : syntax-rules AdviceGlottalStopArchDepth StrokeWidthBlend ArchDepthAOf ArchDepthBOf SmoothAdjust MidJutSide MidJutCenter compositeBaseAnchors YSmoothMidR YSmoothMidL HSwToV NarrowUnicodeT WideUnicodeT VERY-FAR TINY] - define spiroFnImports `[g4 g2 corner flat curl close end straight g2c cg2 flatc ccurl widths - disable-contrast heading unimportant important alsoThru alsoThruThem bezControls + define spiroFnImports `[g4 g2 corner flat curl virt close end straight g2c cg2 flatc ccurl + widths disable-contrast heading unimportant important alsoThru alsoThruThem bezControls quadControls archv arcvh dispiro spiro-outline spiro-collect] define booleFnImports `[union intersection difference] - define drvCoordImports `[pre@ post@ mix@ pre@slope post@slope + define drvCoordImports `[pre@ post@ mix@ mix@rev pre@slope post@slope decor@ decor@@ decor@@@ pre@tang-in post@tang-in pre@tang-out post@tang-out] dirty `[$GlyphBlocks$.push : lambda [$Capture_Ext$] : begin \\ diff --git a/packages/font-kits/src/derived-coordinates.mjs b/packages/font-kits/src/derived-coordinates.mjs index 1e0d02b4b..efb94eac2 100644 --- a/packages/font-kits/src/derived-coordinates.mjs +++ b/packages/font-kits/src/derived-coordinates.mjs @@ -7,7 +7,6 @@ import { DEP_SAME_X, DEP_SAME_Y, DerivedCoordinateBase, - InterpolatorBase, } from "@iosevka/geometry/spiro-control"; const TINY = 1 / 128; @@ -29,6 +28,7 @@ export function SetupBuilders(_bindings) { // mix@: mix between pre and post point's X or Y coordinates // usage [mix@ proportion] or [mix@ proportion delta] "mix@": (p, delta) => new CMixCoord(p, delta), + "mix@rev": (p, delta) => new CMixCoord(1 - p, delta), // pre@slope, post@slope: Get the coordiante using the pre/post point's coordinate and a // slope. An optional delta can be added to the result. See the definitions for more @@ -169,3 +169,29 @@ class CAtSlopePost extends DerivedCoordinateBase { return post.x + (curr.y - post.y) / this.slope + this.delta; } } + +const KBP_X = 1; +const KBP_Y = 2; +export class CopyBackKnotProxy { + constructor(delegate, bag, options) { + this.delegate = delegate; + this.bag = bag; + this.options = options; + } + + getDependency(stage) { + return this.delegate.getDependency(stage); + } + getKernelKnot() { + return this.delegate.getKernelKnot(); + } + resolveCoordiantePropogation(ic, pre, post) { + let r = this.delegate.resolveCoordiantePropogation(ic, pre, post); + if (this.options & KBP_X) this.bag.x = this.delegate.x; + if (this.options & KBP_Y) this.bag.y = this.delegate.y; + return r; + } + resolveInterpolation() { + return this.delegate.resolveInterpolation(); + } +} diff --git a/packages/font-kits/src/spiro-kit.mjs b/packages/font-kits/src/spiro-kit.mjs index f877c26ac..ee24410c0 100644 --- a/packages/font-kits/src/spiro-kit.mjs +++ b/packages/font-kits/src/spiro-kit.mjs @@ -1,71 +1,68 @@ import { DiSpiroGeometry, SpiroGeometry } from "@iosevka/geometry"; import { - BiKnotCollector, Interpolator, SpiroFlattener, TerminateInstruction, UserCloseKnotPair, UserControlKnot, + VirtualControlKnot, } from "@iosevka/geometry/spiro-control"; import { bez3, fallback, mix } from "@iosevka/util"; +import { BiKnotCollector } from "../../geometry/src/spiro-expand.mjs"; /////////////////////////////////////////////////////////////////////////////////////////////////// class SpiroImplBase { - constructor(bindings, args) { + constructor(bindings, controls) { this.bindings = bindings; - this.args = args; + this.controls = controls; } - createCollector(glyph) { - const gizmo = glyph.gizmo || this.bindings.GlobalTransform; - + collectTo(collector) { const flattener = new SpiroFlattener(); - for (const control of this.args) flattener.add(control); + for (const control of this.controls) flattener.add(control); flattener.flatten(); - - const collector = new BiKnotCollector(this.bindings.Contrast); flattener.pipe(collector); - - return { gizmo, collector }; } } class DispiroImpl extends SpiroImplBase { - constructor(bindings, args) { - super(bindings, args); + constructor(bindings, controls) { + super(bindings, controls); } applyToGlyph(glyph) { - const { gizmo, collector } = this.createCollector(glyph); + const gizmo = glyph.gizmo || this.bindings.GlobalTransform; + const collector = new BiKnotCollector(this.bindings.Contrast); + this.collectTo(collector); const dsp = new DiSpiroProxy(gizmo, collector); glyph.includeGeometry(dsp.geometry); return dsp; } } + class SpiroOutlineImpl extends SpiroImplBase { - constructor(bindings, args) { - super(bindings, args); + constructor(bindings, controls) { + super(bindings, controls); } applyToGlyph(glyph) { - const { gizmo, collector } = this.createCollector(glyph); + const gizmo = glyph.gizmo || this.bindings.GlobalTransform; + const collector = new BiKnotCollector(this.bindings.Contrast); + this.collectTo(collector); return glyph.includeGeometry( - new SpiroGeometry( - gizmo, - collector.closed, - collector.controls.map(k => k.toMono()), - ), + new SpiroGeometry(gizmo, collector.closed, collector.getMonoKnots()), ); } } + class DiSpiroProxy { constructor(gizmo, collector) { this.geometry = new DiSpiroGeometry( gizmo, collector.contrast, collector.closed, - collector.controls, + collector.knots, ); - this.m_origKnots = collector.controls; + this.m_origKnots = collector.knots; } get knots() { return this.m_origKnots; @@ -87,6 +84,12 @@ function KnotType(type) { }; } +function virtualKnot(x, y, f) { + if (!UserControlKnot.isCoordinateValid(x)) throw new TypeError("NaN detected for X"); + if (!UserControlKnot.isCoordinateValid(y)) throw new TypeError("NaN detected for Y"); + return new VirtualControlKnot(x, y, f); +} + /// The builder for directed knot pairs class DirectedKnotPairBuilder { constructor(bindings, kPre, kCenter, kPost, deltaX, deltaY) { @@ -113,6 +116,7 @@ export function SetupBuilders(bindings) { const corner = KnotType("corner"); const flat = KnotType("left"); const curl = KnotType("right"); + const virt = virtualKnot; const close = f => new TerminateInstruction("close", f); const end = f => new TerminateInstruction("end", f); @@ -401,16 +405,22 @@ export function SetupBuilders(bindings) { const s = fallback(_s, Superness); return 1 - Math.pow(1 - Math.pow(px, s), 1 / s); }; + archv.sCos = function (angle, _s) { + return Math.pow(Math.cos((angle / 180) * Math.PI), 2 / fallback(_s, Superness)); + }; + archv.sSin = function (angle, _s) { + return Math.pow(Math.sin((angle / 180) * Math.PI), 2 / fallback(_s, Superness)); + }; - function dispiro(...args) { - return new DispiroImpl(bindings, args); + function dispiro(...controls) { + return new DispiroImpl(bindings, controls); } - function spiroOutline(...args) { - return new SpiroOutlineImpl(bindings, args); + function spiroOutline(...controls) { + return new SpiroOutlineImpl(bindings, controls); } - function spiroCollect(glyph, ...args) { - const spb = new SpiroImplBase(bindings, args); - return spb.createCollector(glyph); + function spiroCollect(collector, ...controls) { + const spb = new SpiroImplBase(bindings, controls); + return spb.collectTo(collector); } return { @@ -419,6 +429,7 @@ export function SetupBuilders(bindings) { corner, flat, curl, + virt, close, end, straight, diff --git a/packages/geometry-cache/src/index.mjs b/packages/geometry-cache/src/index.mjs index 37458b4a0..c8698f214 100644 --- a/packages/geometry-cache/src/index.mjs +++ b/packages/geometry-cache/src/index.mjs @@ -5,7 +5,7 @@ import zlib from "zlib"; import * as CurveUtil from "@iosevka/geometry/curve-util"; import { encode, decode } from "@msgpack/msgpack"; -const Edition = 44; +const Edition = 45; const MAX_AGE = 16; class GfEntry { constructor(age, value) { diff --git a/packages/geometry/package.json b/packages/geometry/package.json index 315eaa75b..995049770 100644 --- a/packages/geometry/package.json +++ b/packages/geometry/package.json @@ -10,7 +10,9 @@ "./curve-util": "./src/curve-util.mjs", "./point": "./src/point.mjs", "./transform": "./src/transform.mjs", - "./spiro-control": "./src/spiro-control.mjs" + "./spiro-control": "./src/spiro-control.mjs", + "./spiro-expand": "./src/spiro-expand.mjs", + "./spiro-pen-expand": "./src/spiro-pen-expand.mjs" }, "dependencies": { "@iosevka/util": "31.1.0", diff --git a/packages/geometry/src/index.mjs b/packages/geometry/src/index.mjs index 136061c1b..ddf829210 100644 --- a/packages/geometry/src/index.mjs +++ b/packages/geometry/src/index.mjs @@ -5,7 +5,7 @@ import * as CurveUtil from "./curve-util.mjs"; import { Point } from "./point.mjs"; import { QuadifySink } from "./quadify.mjs"; import { SpiroExpander } from "./spiro-expand.mjs"; -import { createSpiroPenGeometry } from "./spiro-pen-expander.mjs"; +import { PenSpiroExpander } from "./spiro-pen-expand.mjs"; import { spiroToOutlineWithSimplification } from "./spiro-to-outline.mjs"; import { strokeArcs } from "./stroke.mjs"; import { Transform } from "./transform.mjs"; @@ -141,23 +141,23 @@ export class SpiroGeometry extends CachedGeometry { } export class SpiroPenGeometry extends CachedGeometry { - constructor(gizmo, closed, pen, knots) { + constructor(gizmo, penProfile, closed, knots) { super(); this.m_gizmo = gizmo; + this.m_penProfile = penProfile; this.m_closed = closed; this.m_knots = knots; - this.m_pen = pen; } toContoursImpl() { - let contours = createSpiroPenGeometry( + const expander = new PenSpiroExpander( this.m_gizmo, + this.m_penProfile, this.m_closed, this.m_knots, - this.m_pen, ); - - if (!contours.length) return []; + let contours = expander.getGeometry(); + if (!contours || !contours.length) return []; let stack = []; for (const [i, c] of contours.entries()) { @@ -189,7 +189,7 @@ export class SpiroPenGeometry extends CachedGeometry { measureComplexity() { let cplx = CPLX_NON_EMPTY | CPLX_NON_SIMPLE; - for (const z of this.m_pen) { + for (const z of this.m_penProfile) { if (!isFinite(z.x) || !isFinite(z.y)) cplx |= CPLX_BROKEN; } for (const z of this.m_knots) { @@ -204,8 +204,8 @@ export class SpiroPenGeometry extends CachedGeometry { h.bool(this.m_closed); // Serialize the pen - h.beginArray(this.m_pen.length); - for (const z of this.m_pen) h.point(z); + h.beginArray(this.m_penProfile.length); + for (const z of this.m_penProfile) h.point(z); h.endArray(); // Serialize the knots diff --git a/packages/geometry/src/spiro-control.mjs b/packages/geometry/src/spiro-control.mjs index cbfe7af42..9506b1fd2 100644 --- a/packages/geometry/src/spiro-control.mjs +++ b/packages/geometry/src/spiro-control.mjs @@ -37,6 +37,7 @@ export class SpiroFlattener { for (const fn of this.preControlFunctions) fn.call(collector); for (const control of this.controls) collector.pushKnot(control); for (const postControl of this.postControls) postControl.applyTo(collector); + collector.finish(); } /// Add a control object (or list) to a sink @@ -326,6 +327,28 @@ export class UserCloseKnotPair { } } +export class VirtualControlKnot { + constructor(x, y, af) { + this.center = new UserControlKnot("corner", x, y, af); + } + + getDependency(stage) { + return this.center.getDependency(stage); + } + getKernelKnot() { + return this.center.getKernelKnot(); + } + resolveCoordiantePropogation(ic, pre, post) { + this.center.resolveCoordiantePropogation(ic, pre, post); + } + resolveNonInterpolated() { + return []; + } + resolveInterpolation() { + throw new Error("Unreachable"); + } +} + export class InterpolatorBase { constructor() {} @@ -415,6 +438,8 @@ export function WithKnotProxy(proxy, actual) { return new KnotProxyInterpolator(proxy, actual); } +/////////////////////////////////////////////////////////////////////////////////////////////////// + export class TerminateInstruction { constructor(type, af) { this.type = type; @@ -443,139 +468,3 @@ export class DerivedCoordinateBase { throw new Error("Unimplemented"); } } - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -export class BiKnotCollector { - constructor(contrast) { - this.contrast = contrast; // stroke contrast - this.defaultD1 = 0; // default LHS - this.defaultD2 = 0; // default RHS sw - this.lastKnot = null; // last knot in the processed items - - this.controls = []; // all the control items - this.closed = false; // whether the shape is closed - } - - pushKnot(c) { - let k; - if (this.lastKnot) { - k = new BiKnot(c.type, c.x, c.y, this.lastKnot.d1, this.lastKnot.d2); - } else { - k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2); - } - - this.controls.push(k); - this.lastKnot = k; - - c.applyTo(this); - } - setWidth(l, r) { - if (this.lastKnot) { - this.lastKnot.d1 = l; - this.lastKnot.d2 = r; - } else { - this.defaultD1 = l; - this.defaultD2 = r; - } - } - headsTo(direction) { - if (this.lastKnot) { - this.lastKnot.proposedNormal = direction; - } - } - setUnimportant() { - if (this.lastKnot) { - this.lastKnot.unimportant = 1; - } - } - setContrast(c) { - this.contrast = c; - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -export class MonoKnot { - constructor(type, unimportant, x, y) { - this.type = type; - this.x = x; - this.y = y; - this.unimportant = unimportant; - } - clone() { - const k1 = new MonoKnot(this.type, this.x, this.y, this.unimportant); - return k1; - } - hash(h) { - h.beginStruct("MonoKnot"); - h.str(this.type); - h.bool(this.unimportant); - h.f64(this.x); - h.f64(this.y); - h.endStruct(); - } - - reverseType() { - if (this.type === "left") { - this.type = "right"; - } else if (this.type === "right") { - this.type = "left"; - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -class BiKnot { - constructor(type, x, y, d1, d2) { - this.type = type; - this.x = x; - this.y = y; - this.d1 = d1; - this.d2 = d2; - this.proposedNormal = null; - this.unimportant = 0; - - // Derived properties - this.origTangent = null; - } - clone() { - const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2); - k1.origTangent = this.origTangent; - k1.proposedNormal = this.proposedNormal; - k1.unimportant = this.unimportant; - return k1; - } - withGizmo(gizmo) { - const tfZ = gizmo.applyXY(this.x, this.y); - const k1 = new BiKnot(this.type, tfZ.x, tfZ.y, this.d1, this.d2); - k1.origTangent = this.origTangent ? gizmo.applyOffset(this.origTangent) : null; - k1.proposedNormal = this.proposedNormal ? gizmo.applyOffset(this.proposedNormal) : null; - k1.unimportant = this.unimportant; - return k1; - } - hash(h) { - h.beginStruct("BiKnot"); - h.str(this.type); - h.bool(this.unimportant); - h.f64(this.x); - h.f64(this.y); - - h.bool(this.d1 != null); - if (this.d1 != null) h.f64(this.d1); - h.bool(this.d2 != null); - if (this.d2 != null) h.f64(this.d2); - - h.bool(this.proposedNormal != null); - if (this.proposedNormal) { - h.f64(this.proposedNormal.x); - h.f64(this.proposedNormal.y); - } - h.endStruct(); - } - - toMono() { - return new MonoKnot(this.type, this.unimportant, this.x, this.y); - } -} diff --git a/packages/geometry/src/spiro-expand.mjs b/packages/geometry/src/spiro-expand.mjs index bd4fe808c..beb497de3 100644 --- a/packages/geometry/src/spiro-expand.mjs +++ b/packages/geometry/src/spiro-expand.mjs @@ -2,7 +2,129 @@ import { linreg, mix } from "@iosevka/util"; import * as SpiroJs from "spiro"; import { Vec2 } from "./point.mjs"; -import { MonoKnot } from "./spiro-control.mjs"; +import { MonoKnot } from "./spiro-to-outline.mjs"; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +export class BiKnotCollector { + constructor(contrast) { + this.contrast = contrast; // stroke contrast + this.defaultD1 = 0; // default LHS + this.defaultD2 = 0; // default RHS sw + this.lastKnot = null; // last knot in the processed items + + this.knots = []; // all the control items + this.closed = false; // whether the shape is closed + + this.m_finished = false; + } + + get controls() { + throw new Error("Not implemented"); + } + + finish() { + this.m_finished = true; + } + pushKnot(c) { + if (this.m_finished) throw new Error("Cannot push knot after finish"); + + let k; + if (this.lastKnot) { + k = new BiKnot(c.type, c.x, c.y, this.lastKnot.d1, this.lastKnot.d2); + } else { + k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2); + } + + this.knots.push(k); + this.lastKnot = k; + + c.applyTo(this); + } + setWidth(l, r) { + if (this.lastKnot) { + this.lastKnot.d1 = l; + this.lastKnot.d2 = r; + } else { + this.defaultD1 = l; + this.defaultD2 = r; + } + } + headsTo(direction) { + if (this.lastKnot) { + this.lastKnot.proposedNormal = direction; + } + } + setUnimportant() { + if (this.lastKnot) { + this.lastKnot.unimportant = 1; + } + } + setContrast(c) { + this.contrast = c; + } + + getMonoKnots() { + let a = []; + for (const c of this.knots) { + a.push(c.toMono()); + } + return a; + } +} + +export class BiKnot { + constructor(type, x, y, d1, d2) { + this.type = type; + this.x = x; + this.y = y; + this.d1 = d1; + this.d2 = d2; + this.proposedNormal = null; + this.unimportant = 0; + + // Derived properties + this.origTangent = null; + } + clone() { + const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2); + k1.origTangent = this.origTangent; + k1.proposedNormal = this.proposedNormal; + k1.unimportant = this.unimportant; + return k1; + } + withGizmo(gizmo) { + const tfZ = gizmo.applyXY(this.x, this.y); + const k1 = new BiKnot(this.type, tfZ.x, tfZ.y, this.d1, this.d2); + k1.origTangent = this.origTangent ? gizmo.applyOffset(this.origTangent) : null; + k1.proposedNormal = this.proposedNormal ? gizmo.applyOffset(this.proposedNormal) : null; + k1.unimportant = this.unimportant; + return k1; + } + hash(h) { + h.beginStruct("BiKnot"); + h.str(this.type); + h.bool(this.unimportant); + h.f64(this.x); + h.f64(this.y); + + h.bool(this.d1 != null); + if (this.d1 != null) h.f64(this.d1); + h.bool(this.d2 != null); + if (this.d2 != null) h.f64(this.d2); + + h.bool(this.proposedNormal != null); + if (this.proposedNormal) { + h.f64(this.proposedNormal.x); + h.f64(this.proposedNormal.y); + } + h.endStruct(); + } + + toMono() { + return new MonoKnot(this.type, this.unimportant, this.x, this.y); + } +} /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -78,19 +200,47 @@ export class SpiroExpander { return { lhs: lhsT, rhs: rhsT, lhsUntransformed: lhsU, rhsUntransformed: rhsU }; } interpolateUnimportantKnots(lhsT, rhsT, lhsU, rhsU) { - for (let j = 0; j < this.m_biKnotsU.length; j++) { - const knotU = this.m_biKnotsU[j]; - if (!knotU.unimportant) continue; - let jBefore, jAfter; - for (jBefore = j - 1; cyNth(this.m_biKnotsU, jBefore).unimportant; jBefore--); - for (jAfter = j + 1; cyNth(this.m_biKnotsU, jAfter).unimportant; jAfter++); + let firstImportantIdx = -1; + let lastImportantIdx = -1; - const knotUBefore = cyNth(this.m_biKnotsU, jBefore), - knotUAfter = cyNth(this.m_biKnotsU, jAfter), - lhsUBefore = cyNth(lhsU, jBefore), - lhsUAfter = cyNth(lhsU, jAfter), - rhsUBefore = cyNth(rhsU, jBefore), - rhsUAfter = cyNth(rhsU, jAfter); + for (let j = 0; j < this.m_biKnotsU.length; j++) { + // If the current knot is unimportant, skip it + if (this.m_biKnotsU[j].unimportant) continue; + + // If we've scanned an important knot before, interpolate the unimportant knots between + if (lastImportantIdx !== -1) { + this.interpolateUnimportantKnotsRg(lhsT, rhsT, lhsU, rhsU, lastImportantIdx, j); + } + + if (firstImportantIdx === -1) firstImportantIdx = j; + lastImportantIdx = j; + } + + // Handle the last important ... first important wraparound + if (firstImportantIdx !== -1 && lastImportantIdx !== -1) { + this.interpolateUnimportantKnotsRg( + lhsT, + rhsT, + lhsU, + rhsU, + lastImportantIdx, + firstImportantIdx, + ); + } + } + + interpolateUnimportantKnotsRg(lhsT, rhsT, lhsU, rhsU, jBefore, jAfter) { + let count = jAfter > jBefore ? jAfter - jBefore : lhsT.length - jBefore + jAfter; + for (let offset = 1; offset < count; offset++) { + let j = (jBefore + offset) % lhsT.length; + + const knotUBefore = this.m_biKnotsU[jBefore], + knotU = this.m_biKnotsU[j], + knotUAfter = this.m_biKnotsU[jAfter], + lhsUBefore = lhsU[jBefore], + lhsUAfter = lhsU[jAfter], + rhsUBefore = rhsU[jBefore], + rhsUAfter = rhsU[jAfter]; lhsU[j].x = linreg(knotUBefore.x, lhsUBefore.x, knotUAfter.x, lhsUAfter.x, knotU.x); lhsU[j].y = linreg(knotUBefore.y, lhsUBefore.y, knotUAfter.y, lhsUAfter.y, knotU.y); diff --git a/packages/geometry/src/spiro-pen-expand.mjs b/packages/geometry/src/spiro-pen-expand.mjs new file mode 100644 index 000000000..8fcdcc303 --- /dev/null +++ b/packages/geometry/src/spiro-pen-expand.mjs @@ -0,0 +1,243 @@ +import * as SpiroJs from "spiro"; + +import { linreg } from "@iosevka/util"; +import * as CurveUtil from "./curve-util.mjs"; +import { Point } from "./point.mjs"; +import { MonoKnot } from "./spiro-to-outline.mjs"; + +export class PenKnotCollector { + constructor(gizmo, defaultProfile) { + this.gizmo = gizmo; + this.m_profile = defaultProfile; + this.m_lastKnot = null; + this.m_finished = false; + + this.knots = []; + this.closed = false; + } + finish() { + this.m_finished = true; + } + pushKnot(c) { + if (this.m_finished) throw new Error("Cannot push knot after finish"); + let k = new PenKnot(c.type, c.x, c.y, this.m_profile); + this.knots.push(k); + this.m_lastKnot = k; + + c.applyTo(this); + } + + setWidth() {} + headsTo() {} + setUnimportant() { + if (this.m_lastKnot) this.m_lastKnot.profile = null; + } + setContrast() {} + + setProfile(profile) { + if (profile.length !== this.m_profile.length) + throw new Error("Pen profile length mismatch"); + if (this.m_lastKnot) this.m_lastKnot.profile = profile; + this.m_profile = profile; + } +} + +export class PenKnot { + constructor(type, x, y, profile) { + this.type = type; + this.x = x; + this.y = y; + this.profile = profile; + } + clone() { + const k1 = new PenKnot(this.type, this.x, this.y, this.profile); + return k1; + } + withGizmo(gizmo) { + const tfZ = gizmo.applyXY(this.x, this.y); + const k1 = new PenKnot(this.type, tfZ.x, tfZ.y, this.profile); + return k1; + } + hash(h) { + h.beginStruct("PenKnot"); + h.str(this.type); + h.f64(this.x); + h.f64(this.y); + h.bool(this.profile != null); + if (this.profile) { + h.beginArray(this.profile.length); + for (let i = 0; i < this.profile.length; i++) { + h.f64(this.profile[i].x); + h.f64(this.profile[i].y); + } + h.endArray(); + } + h.endStruct(); + } +} + +export class PenSpiroExpander { + constructor(gizmo, profile, closed, knots) { + this.m_gizmo = gizmo; + this.m_closed = closed; + this.m_knotsU = Array.from(knots); + this.m_knotsT = knots.map(k => k.withGizmo(gizmo)); + this.m_profileEdges = profile.length; + + this.m_traces = []; + } + + getGeometry() { + this.traceAll(); + + const contours = []; + for (let i = 0; i < this.m_traces.length; i++) { + const iNext = (i + 1) % this.m_traces.length; + if (this.m_traces[i].length !== this.m_traces[iNext].length) { + throw new Error("Different number of arcs in traces"); + } + for (let j = 0; j < this.m_traces[i].length; j++) { + const arcForward = this.m_traces[i][j]; + const arcBackward = this.m_traces[iNext][j]; + makeProfiledStroke(contours, arcForward, arcBackward); + } + } + + return contours; + } + + traceAll() { + for (let i = 0; i < this.m_profileEdges; i++) { + this.calculateShiftedProfile(i); + } + } + calculateShiftedProfile(iEdge) { + let traceT = [], + traceU = []; + for (let i = 0; i < this.m_knotsT.length; i++) { + const trT = this.getTrace(this.m_knotsT[i], iEdge); + const trU = trT.clone(); + this.m_gizmo.unapplyToSink(trT, trU); + traceT.push(trT), traceU.push(trU); + } + this.interpolateUnimportantTraceKnots(traceU, traceT); + + let arcc = new SimplyCollectArcs(); + SpiroJs.spiroToArcsOnContext(traceT, this.m_closed, arcc); + this.m_traces.push(arcc.arcs); + } + getTrace(k, iEdge) { + if (k.profile) { + return new MonoKnot(k.type, false, k.x + k.profile[iEdge].x, k.y + k.profile[iEdge].y); + } else { + return new MonoKnot(k.type, true, k.x, k.y); + } + } + interpolateUnimportantTraceKnots(traceU, traceT) { + let firstImportantIdx = -1; + let lastImportantIdx = -1; + + for (let i = 0; i < traceU.length; i++) { + if (traceU[i].unimportant) continue; + + if (lastImportantIdx !== -1) { + this.interpolateUnimportantTraceKnotsRg(traceU, traceT, lastImportantIdx, i); + } + + if (firstImportantIdx === -1) firstImportantIdx = i; + lastImportantIdx = i; + } + + if (firstImportantIdx !== -1 && lastImportantIdx !== -1) { + this.interpolateUnimportantTraceKnotsRg( + traceU, + traceT, + lastImportantIdx, + firstImportantIdx, + ); + } + } + interpolateUnimportantTraceKnotsRg(traceU, traceT, last, next) { + let count = next > last ? next - last : traceU.length - last + next; + for (let offset = 1; offset < count; offset++) { + let i = (last + offset) % traceU.length; + this.interpolateKnot( + this.m_knotsU[last], + traceU[last], + this.m_knotsU[i], + traceU[i], + traceT[i], + this.m_knotsU[next], + traceU[next], + ); + } + } + interpolateKnot(rBefore, uBefore, rCurr, uCurr, tCurr, rAfter, uAfter) { + uCurr.x = linreg(rBefore.x, uBefore.x, rAfter.x, uAfter.x, rCurr.x); + uCurr.y = linreg(rBefore.y, uBefore.y, rAfter.y, uAfter.y, rCurr.y); + this.m_gizmo.applyToSink(uCurr, tCurr); + } +} + +class SimplyCollectArcs { + constructor() { + this.arcs = []; + } + beginShape() {} + endShape() {} + moveTo() {} + arcTo(arc) { + this.arcs.push(arc); + } +} + +// arcForward and arcBackward must be spiro arcs. +function makeProfiledStroke(contours, arcForward, arcBackward) { + const [subdividesForward, subdividesBackward] = subdivideKnotPair( + arcForward, + arcBackward, + CurveUtil.GEOMETRY_PRECISION, + ); + for (let i = 0; i < subdividesForward.length; i++) { + const [a1, b1, c1, d1] = subdividesForward[i].toCubicBezier(); + const [a2, b2, c2, d2] = subdividesBackward[i].toCubicBezier(); + contours.push([ + Point.from(Point.Type.Corner, a1), + Point.from(Point.Type.CubicStart, b1), + Point.from(Point.Type.CubicEnd, c1), + Point.from(Point.Type.Corner, d1), + Point.from(Point.Type.Corner, d2), + Point.from(Point.Type.CubicStart, c2), + Point.from(Point.Type.CubicEnd, b2), + Point.from(Point.Type.Corner, a2), + ]); + } +} + +function subdivideKnotPair(arcForward, arcBackward, delta) { + const MAX_STOPS = 16; + + let sinkForward = [], + sinkBackward = []; + for (let stops = 1; stops < MAX_STOPS; stops++) { + sinkForward.length = 0; + sinkBackward.length = 0; + + uniformSubdivide(arcForward, stops, sinkForward); + uniformSubdivide(arcBackward, stops, sinkBackward); + + let flatEnough = true; + for (const fwd of sinkForward) if (fwd.bend > delta) flatEnough = false; + for (const bwd of sinkBackward) if (bwd.bend > delta) flatEnough = false; + + if (flatEnough) break; + } + return [sinkForward, sinkBackward]; +} +function uniformSubdivide(arc, stops, sink) { + for (; stops > 1; stops--) { + const f = arc.subdivide(1 / stops); + sink.push(f[0]), (arc = f[1]); + } + sink.push(arc); +} diff --git a/packages/geometry/src/spiro-pen-expander.mjs b/packages/geometry/src/spiro-pen-expander.mjs deleted file mode 100644 index ab5284ea9..000000000 --- a/packages/geometry/src/spiro-pen-expander.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import * as SpiroJs from "spiro"; -import * as CurveUtil from "./curve-util.mjs"; - -import { Point } from "./point.mjs"; - -export function createSpiroPenGeometry(gizmo, closed, knots, pen) { - const collector = new ArcCollector(gizmo, pen); - SpiroJs.spiroToBezierOnContext(knots, closed, collector, CurveUtil.GEOMETRY_PRECISION); - return collector.contoursCollected; -} - -class ArcCollector { - constructor(gizmo, pen) { - this.gizmo = gizmo; - this.lastX = 0; - this.lastY = 0; - this.m_pen = pen; - this.contoursCollected = []; - } - - beginShape() {} - endShape() {} - - moveTo(x, y) { - const lastTf = this.gizmo.applyXY(x, y); - this.lastX = lastTf.x; - this.lastY = lastTf.y; - this.addPenProfileAt(this.lastX, this.lastY); - } - - lineTo(x1, y1) { - const z1 = this.gizmo.applyXY(x1, y1); - for (let i = 0; i < this.m_pen.length; i++) { - let penPrev = this.m_pen[i]; - let penNext = this.m_pen[(i + 1) % this.m_pen.length]; - - const l1 = new Point(Point.Type.Corner, this.lastX + penPrev.x, this.lastY + penPrev.y); - const l2 = new Point(Point.Type.Corner, z1.x + penPrev.x, z1.y + penPrev.y); - const r2 = new Point(Point.Type.Corner, z1.x + penNext.x, z1.y + penNext.y); - const r1 = new Point(Point.Type.Corner, this.lastX + penNext.x, this.lastY + penNext.y); - - this.contoursCollected.push([l1, l2, r2, r1]); - } - this.lastX = z1.x; - this.lastY = z1.y; - this.addPenProfileAt(this.lastX, this.lastY); - } - - cubicTo(x2, y2, x3, y3, x4, y4) { - const z2 = this.gizmo.applyXY(x2, y2); - const z3 = this.gizmo.applyXY(x3, y3); - const z4 = this.gizmo.applyXY(x4, y4); - for (let i = 0; i < this.m_pen.length; i++) { - let penPrev = this.m_pen[i]; - let penNext = this.m_pen[(i + 1) % this.m_pen.length]; - - const l1 = new Point(Point.Type.Corner, this.lastX + penPrev.x, this.lastY + penPrev.y); - const l2 = new Point(Point.Type.CubicStart, z2.x + penPrev.x, z2.y + penPrev.y); - const l3 = new Point(Point.Type.CubicEnd, z3.x + penPrev.x, z3.y + penPrev.y); - const l4 = new Point(Point.Type.Corner, z4.x + penPrev.x, z4.y + penPrev.y); - const r4 = new Point(Point.Type.Corner, z4.x + penNext.x, z4.y + penNext.y); - const r3 = new Point(Point.Type.CubicStart, z3.x + penNext.x, z3.y + penNext.y); - const r2 = new Point(Point.Type.CubicEnd, z2.x + penNext.x, z2.y + penNext.y); - const r1 = new Point(Point.Type.Corner, this.lastX + penNext.x, this.lastY + penNext.y); - - this.contoursCollected.push([l1, l2, l3, l4, r4, r3, r2, r1]); - } - this.lastX = z4.x; - this.lastY = z4.y; - this.addPenProfileAt(this.lastX, this.lastY); - } - - addPenProfileAt(x, y) { - let c = []; - for (let i = 0; i < this.m_pen.length; i++) { - let pen = this.m_pen[i]; - c.push(new Point(Point.Type.Corner, x + pen.x, y + pen.y)); - } - this.contoursCollected.push(c); - } -} diff --git a/packages/geometry/src/spiro-to-outline.mjs b/packages/geometry/src/spiro-to-outline.mjs index 1155254e8..f323c95ce 100644 --- a/packages/geometry/src/spiro-to-outline.mjs +++ b/packages/geometry/src/spiro-to-outline.mjs @@ -21,6 +21,38 @@ export function spiroToOutlineWithSimplification(knots, fClosed, gizmo) { return sink.contours; } +/////////////////////////////////////////////////////////////////////////////////////////////////// + +export class MonoKnot { + constructor(type, unimportant, x, y) { + this.type = type; + this.unimportant = unimportant; + this.x = x; + this.y = y; + } + clone() { + return new MonoKnot(this.type, this.unimportant, this.x, this.y); + } + hash(h) { + h.beginStruct("MonoKnot"); + h.str(this.type); + h.bool(this.unimportant); + h.f64(this.x); + h.f64(this.y); + h.endStruct(); + } + + reverseType() { + if (this.type === "left") { + this.type = "right"; + } else if (this.type === "right") { + this.type = "left"; + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + class SpiroSimplifier { constructor(knots) { this.m_knots = knots;