http://mathling.com/geometric/edge library module
http://mathling.com/geometric/edge
Edges: including arcs, quads, and cubics
Edges are decorated with properties which art infrastructure may care
about for rendering purposes.
Properties of note:
r: radius of arc for arc edges; default is none
width: width for stroke; default=d
label: label for this edge
class, colour, opacity: direct styling overrides
Copyright© Mary Holstege 2020-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)
Status: Incomplete, subject to refactoring
Imports
http://mathling.com/core/utilitiesimport module namespace util="http://mathling.com/core/utilities" at "../core/utilities.xqy"http://mathling.com/core/vector
import module namespace v="http://mathling.com/core/vector" at "../core/vector.xqy"http://mathling.com/geometric/box
import module namespace box="http://mathling.com/geometric/box" at "../geo/box.xqy"http://mathling.com/geometric/ellipse
import module namespace ellipse="http://mathling.com/geometric/ellipse" at "../geo/ellipse.xqy"http://mathling.com/core/roots
import module namespace roots="http://mathling.com/core/roots" at "../core/roots.xqy"http://mathling.com/geometric/affine
import module namespace affine="http://mathling.com/geometric/affine" at "../geo/affine.xqy"http://mathling.com/core/config
import module namespace config="http://mathling.com/core/config" at "../core/config.xqy"http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors" at "../core/errors.xqy"http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point" at "../geo/point.xqy"
Variables
Variable: $precision as xs:integer
Variable: $EDGE-RESERVED as xs:string*
Variable: $ARC-RESERVED as xs:string*
Variable: $ELLIPSE-ARC-RESERVED as xs:string*
Variable: $QUAD-RESERVED as xs:string*
Variable: $CUBIC-RESERVED as xs:string*
Functions
Function: edge
declare function edge($from as map(xs:string,item()*),
$to as map(xs:string,item()*),
$d as xs:double,
$properties as map(xs:string,item()*)) as map(xs:string,item()*)
declare function edge($from as map(xs:string,item()*), $to as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*)) as map(xs:string,item()*)
edge()
Make an edge
Params
- from as map(xs:string,item()*): starting point
- to as map(xs:string,item()*): ending point
- d as xs:double: weight of the edge (used by spanning graph as distance)
- properties as map(xs:string,item()*): additional edge properties
Returns
- map(xs:string,item()*)
declare function this:edge( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*) ) as map(xs:string,item()*) { util:merge-into($properties, map { "kind": "edge", "u": $from, "v": $to, "d": $d } ) }
Function: edge
declare function edge($from as map(xs:string,item()*),
$to as map(xs:string,item()*),
$d as xs:double) as map(xs:string,item()*)
declare function edge($from as map(xs:string,item()*), $to as map(xs:string,item()*), $d as xs:double) as map(xs:string,item()*)
edge()
Make an edge
Params
- from as map(xs:string,item()*): starting point
- to as map(xs:string,item()*): ending point
- d as xs:double: the weight of the edge
Returns
- map(xs:string,item()*)
declare function this:edge( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $d as xs:double ) as map(xs:string,item()*) { map { "kind": "edge", "u": $from, "v": $to, "d": $d } }
Function: edge
declare function edge($from as map(xs:string,item()*),
$to as map(xs:string,item()*)) as map(xs:string,item()*)
declare function edge($from as map(xs:string,item()*), $to as map(xs:string,item()*)) as map(xs:string,item()*)
edge()
Make an edge
Params
- from as map(xs:string,item()*): starting point
- to as map(xs:string,item()*): ending point
Returns
- map(xs:string,item()*)
declare function this:edge( $from as map(xs:string,item()*), $to as map(xs:string,item()*) ) as map(xs:string,item()*) { this:edge($from, $to, 0) }
Function: kind
declare function kind($edge as map(xs:string,item()*)) as xs:string
declare function kind($edge as map(xs:string,item()*)) as xs:string
Params
- edge as map(xs:string,item()*)
Returns
- xs:string
declare function this:kind($edge as map(xs:string,item()*)) as xs:string { $edge("kind") }
Function: start
declare function start($edge as map(xs:string,item()*)) as map(xs:string,item()*)
declare function start($edge as map(xs:string,item()*)) as map(xs:string,item()*)
start()
Accessor for starting point of edge
Params
- edge as map(xs:string,item()*): The edge
Returns
- map(xs:string,item()*)
declare function this:start($edge as map(xs:string,item()*)) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return $edge("u") case "quad" return $edge("u") case "cubic" return $edge("u") case "arc" return ( let $start := $edge("start") return if (exists($start)) then $start else ( let $circle := $edge("circle") let $center := ellipse:center($circle) let $r := ellipse:radius($circle) let $from := util:radians($edge("from-angle")) return ( point:point( point:px($center)+math:cos($from)*$r, point:py($center)+math:sin($from)*$r ) ) ) ) case "ellipse-arc" return $edge("start") default return errors:error("GEOM-BADREGION", ($edge, "start")) }
Function: start
declare function start($edge as map(xs:string,item()*), $start as map(xs:string,item()*)) as map(xs:string,item()*)
declare function start($edge as map(xs:string,item()*), $start as map(xs:string,item()*)) as map(xs:string,item()*)
start()
Settor for starting point of edge
Params
- edge as map(xs:string,item()*): The edge
- start as map(xs:string,item()*): Starting point of edge
Returns
- map(xs:string,item()*)
declare function this:start($edge as map(xs:string,item()*), $start as map(xs:string,item()*)) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return $edge=>map:put("u", $start) case "quad" return $edge=>map:put("u", $start) case "cubic" return $edge=>map:put("u", $start) case "arc" return this:as-point-arc($edge)=>map:put("start", $start) case "ellipse-arc" return $edge=>map:put("start", $start) default return errors:error("GEOM-BADREGION", ($edge, "start")) }
Function: end
declare function end($edge as map(xs:string,item()*)) as map(xs:string,item()*)
declare function end($edge as map(xs:string,item()*)) as map(xs:string,item()*)
end()
Accessor for ending point of edge
Params
- edge as map(xs:string,item()*): The edge
Returns
- map(xs:string,item()*)
declare function this:end($edge as map(xs:string,item()*)) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return $edge("v") case "quad" return $edge("v") case "cubic" return $edge("v") case "arc" return ( let $end := $edge("end") return if (exists($end)) then $end else ( let $circle := $edge("circle") let $center := ellipse:center($circle) let $r := ellipse:radius($circle) let $to := util:radians($edge("to-angle")) return point:point( point:px($center)+math:cos($to)*$r, point:py($center)+math:sin($to)*$r ) ) ) case "ellipse-arc" return $edge("end") default return errors:error("GEOM-BADREGION", ($edge, "end")) }
Function: weight
declare function weight($edge as map(xs:string,item()*)) as xs:double
declare function weight($edge as map(xs:string,item()*)) as xs:double
weight()
Accessor for the weight of an edge
Params
- edge as map(xs:string,item()*): The edge
Returns
- xs:double
declare function this:weight($edge as map(xs:string,item()*)) as xs:double { if (this:kind($edge) = ("edge","cubic","quad")) then ($edge("d"),0)[1] else 0 }
Function: arc
declare function arc($center as map(xs:string,item()*),
$radius as xs:double,
$start as map(xs:string,item()*),
$end as map(xs:string,item()*),
$flipped as xs:boolean,
$large as xs:boolean) as map(xs:string,item()*)
declare function arc($center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean, $large as xs:boolean) as map(xs:string,item()*)
arc()
Construct an arc (e.g. hyperbolic edge)
Note: calculations on $start and $end need to be correct:
use hyperbolic:arc-through() to get them right; this is really an
internal contructor function. Ad hoc construction of arc should use
arc-by-angle().
Params
- center as map(xs:string,item()*): center of curvature
- radius as xs:double: radius of circle
- start as map(xs:string,item()*): starting point
- end as map(xs:string,item()*): ending point
- flipped as xs:boolean: indicates sweep
- large as xs:boolean: indicated large arc
Returns
- map(xs:string,item()*)
declare function this:arc( $center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean, $large as xs:boolean ) as map(xs:string,item()*) { map { "kind": "arc", "circle": ellipse:circle($center, $radius), "start": $start, "end": $end, "flipped": $flipped, "large": $large } }
Function: arc
declare function arc($center as map(xs:string,item()*),
$radius as xs:double,
$start as map(xs:string,item()*),
$end as map(xs:string,item()*),
$flipped as xs:boolean) as map(xs:string,item()*)*
declare function arc($center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean) as map(xs:string,item()*)*
Params
- center as map(xs:string,item()*)
- radius as xs:double
- start as map(xs:string,item()*)
- end as map(xs:string,item()*)
- flipped as xs:boolean
Returns
- map(xs:string,item()*)*
declare function this:arc( $center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean ) as map(xs:string,item()*)* { this:arc($center, $radius, $start, $end, $flipped, false()) }
Function: arc
declare function arc($center as map(xs:string,item()*),
$radius as xs:double,
$start as map(xs:string,item()*),
$end as map(xs:string,item()*)) as map(xs:string,item()*)
declare function arc($center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- center as map(xs:string,item()*)
- radius as xs:double
- start as map(xs:string,item()*)
- end as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:arc( $center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*) ) as map(xs:string,item()*) { this:arc($center, $radius, $start, $end, false(), false()) }
Function: arc-by-angle
declare function arc-by-angle($center as map(xs:string,item()*),
$radius as xs:double,
$θ as xs:double,
$θ2 as xs:double,
$flipped as xs:boolean,
$large as xs:boolean) as map(xs:string,item()*)
declare function arc-by-angle($center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double, $flipped as xs:boolean, $large as xs:boolean) as map(xs:string,item()*)
arc-by-angle()
Construct an arc
Params
- center as map(xs:string,item()*): center of curvature
- radius as xs:double: radius of circle
- θ as xs:double: starting angle (degrees)2: ending angle (degrees)
- θ2 as xs:double: ending angle (degrees)
- flipped as xs:boolean: indicates sweep
- large as xs:boolean: indicates large arc
Returns
- map(xs:string,item()*)
declare function this:arc-by-angle( $center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double, $flipped as xs:boolean, $large as xs:boolean ) as map(xs:string,item()*) { map { "kind": "arc", "circle": ellipse:circle($center, $radius), "from-angle": $θ, "to-angle": $θ2, "flipped": $flipped, "large": $large } }
Function: arc-by-angle
declare function arc-by-angle($center as map(xs:string,item()*),
$radius as xs:double,
$θ as xs:double,
$θ2 as xs:double,
$flipped as xs:boolean) as map(xs:string,item()*)
declare function arc-by-angle($center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double, $flipped as xs:boolean) as map(xs:string,item()*)
Params
- center as map(xs:string,item()*)
- radius as xs:double
- θ as xs:double
- θ2 as xs:double
- flipped as xs:boolean
Returns
- map(xs:string,item()*)
declare function this:arc-by-angle( $center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double, $flipped as xs:boolean ) as map(xs:string,item()*) { this:arc-by-angle($center, $radius, $θ, $θ2, $flipped, false()) }
Function: arc-by-angle
declare function arc-by-angle($center as map(xs:string,item()*),
$radius as xs:double,
$θ as xs:double,
$θ2 as xs:double) as map(xs:string,item()*)
declare function arc-by-angle($center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double) as map(xs:string,item()*)
Params
- center as map(xs:string,item()*)
- radius as xs:double
- θ as xs:double
- θ2 as xs:double
Returns
- map(xs:string,item()*)
declare function this:arc-by-angle( $center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double ) as map(xs:string,item()*) { this:arc-by-angle($center, $radius, $θ, $θ2, false(), false()) }
Function: arc-circle
declare function arc-circle($arc as map(xs:string,item()*)) as map(xs:string,item()*)
declare function arc-circle($arc as map(xs:string,item()*)) as map(xs:string,item()*)
arc-circle()
Center and radius of curvature for this arc
$arc: The arc
Params
- arc as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:arc-circle($arc as map(xs:string,item()*)) as map(xs:string,item()*) { $arc("circle") }
Function: arc-center
declare function arc-center($arc as map(xs:string,item()*)) as map(xs:string,item()*)
declare function arc-center($arc as map(xs:string,item()*)) as map(xs:string,item()*)
arc-center()
Center of this arc
$arc: The arc
Params
- arc as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:arc-center($arc as map(xs:string,item()*)) as map(xs:string,item()*) { if (this:kind($arc)="ellipse-arc") then $arc("ellipse")=>ellipse:center() else $arc("circle")=>ellipse:center() }
Function: arc-radius
declare function arc-radius($arc as map(xs:string,item()*)) as xs:double
declare function arc-radius($arc as map(xs:string,item()*)) as xs:double
arc-radius()
Radius of curvature of this arc
$arc: The arc
Params
- arc as map(xs:string,item()*)
Returns
- xs:double
declare function this:arc-radius($arc as map(xs:string,item()*)) as xs:double { $arc("circle")=>ellipse:radius() }
Function: arc-angles
declare function arc-angles($arc as map(xs:string,item()*)) as xs:double*
declare function arc-angles($arc as map(xs:string,item()*)) as xs:double*
arc-angles()
Starting and ending angles for the arc (degrees), for arc-by-angle arcs
$arc: The arc
Params
- arc as map(xs:string,item()*)
Returns
- xs:double*
declare function this:arc-angles($arc as map(xs:string,item()*)) as xs:double* { ($arc("from-angle"),$arc("to-angle")) }
Function: arc-ends
declare function arc-ends($arc as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function arc-ends($arc as map(xs:string,item()*)) as map(xs:string,item()*)*
arc-ends()
Starting and ending points for the arc, for arc-by-point arcs
$arc: The arc
Params
- arc as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:arc-ends($arc as map(xs:string,item()*)) as map(xs:string,item()*)* { ($arc("start"),$arc("end")) }
Function: arc-flipped
declare function arc-flipped($arc as map(xs:string,item()*)) as xs:boolean
declare function arc-flipped($arc as map(xs:string,item()*)) as xs:boolean
arc-flipped()
Sweep for the arc
$arc: The arc
Params
- arc as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:arc-flipped($arc as map(xs:string,item()*)) as xs:boolean { (: Default is false :) if ($arc("flipped")) then true() else false() }
Function: arc-large
declare function arc-large($arc as map(xs:string,item()*)) as xs:boolean
declare function arc-large($arc as map(xs:string,item()*)) as xs:boolean
Params
- arc as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:arc-large($arc as map(xs:string,item()*)) as xs:boolean { (: Default is false :) if ($arc("large")) then true() else false() }
Function: as-angle-arc
declare function as-angle-arc($arc as map(xs:string,item()*)) as map(xs:string,item()*)
declare function as-angle-arc($arc as map(xs:string,item()*)) as map(xs:string,item()*)
as-angle-arc()
Shift arc edge from the end-point representation to the angle representation.
Params
- arc as map(xs:string,item()*): input arc
Returns
- map(xs:string,item()*)
declare function this:as-angle-arc($arc as map(xs:string,item()*)) as map(xs:string,item()*) { let $points := $arc=>this:arc-ends() return ( if (exists($points)) then ( let $circle := $arc=>this:arc-circle() let $center := ellipse:center($circle) let $α := this:angle($center, $points[1]) let $ω := this:angle($center, $points[2]) return util:merge-into( this:property-map($arc), this:arc-by-angle( $center, ellipse:radius($circle), $α, if ($ω - $α > 360) then 360 - $ω else $ω, $arc=>this:arc-flipped(), $arc=>this:arc-large() ) ) ) else ( $arc ) ) }
Function: as-point-arc
declare function as-point-arc($arc as map(xs:string,item()*)) as map(xs:string,item()*)
declare function as-point-arc($arc as map(xs:string,item()*)) as map(xs:string,item()*)
as-point-arc()
Shift arc edge from the angle representation to the end-point representation.
Params
- arc as map(xs:string,item()*): input arc
Returns
- map(xs:string,item()*)
declare function this:as-point-arc( $arc as map(xs:string,item()*) ) as map(xs:string,item()*) { let $points := $arc=>this:arc-ends() return ( if (exists($points)) then ( $arc ) else ( let $circle := $arc=>this:arc-circle() return ( util:merge-into( this:property-map($arc), this:arc( ellipse:center($circle), ellipse:radius($circle), $arc=>this:start(), $arc=>this:end(), $arc=>this:arc-flipped(), $arc=>this:arc-large() ) ) ) ) ) }
Function: ellipse-arc
declare function ellipse-arc($center as map(xs:string,item()*),
$a as xs:double,
$b as xs:double,
$rotation as xs:double,
$start as map(xs:string,item()*),
$end as map(xs:string,item()*),
$flipped as xs:boolean,
$large as xs:boolean)
declare function ellipse-arc($center as map(xs:string,item()*), $a as xs:double, $b as xs:double, $rotation as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean, $large as xs:boolean)
Params
- center as map(xs:string,item()*)
- a as xs:double
- b as xs:double
- rotation as xs:double
- start as map(xs:string,item()*)
- end as map(xs:string,item()*)
- flipped as xs:boolean
- large as xs:boolean
declare function this:ellipse-arc( $center as map(xs:string,item()*), $a as xs:double, $b as xs:double, $rotation as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean, $large as xs:boolean ) { map { "kind": "ellipse-arc", "ellipse": ellipse:ellipse($center, $a, $b, $rotation), "start": $start, "end": $end, "flipped": $flipped, "large": $large } }
Function: arc-ellipse
declare function arc-ellipse($ellipse-arc as map(xs:string,item()*)) as map(xs:string,item()*)
declare function arc-ellipse($ellipse-arc as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- ellipse-arc as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:arc-ellipse($ellipse-arc as map(xs:string,item()*)) as map(xs:string,item()*) { $ellipse-arc("ellipse") }
Function: quad
declare function quad($from as map(xs:string,item()*),
$to as map(xs:string,item()*),
$control as map(xs:string,item()*),
$d as xs:double,
$properties as map(xs:string,item()*)) as map(xs:string,item()*)
declare function quad($from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*)) as map(xs:string,item()*)
quad()
Construct a quadratic edge.
Params
- from as map(xs:string,item()*): starting point
- to as map(xs:string,item()*): ending point
- control as map(xs:string,item()*): control point
- d as xs:double: edge weight
- properties as map(xs:string,item()*): additional properties
Returns
- map(xs:string,item()*)
declare function this:quad( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*) ) as map(xs:string,item()*) { util:merge-into($properties, map { "kind": "quad", "u": $from, "v": $to, "c1": $control, "d": $d } ) }
Function: quad
declare function quad($from as map(xs:string,item()*),
$to as map(xs:string,item()*),
$control as map(xs:string,item()*),
$d as xs:double) as map(xs:string,item()*)
declare function quad($from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*), $d as xs:double) as map(xs:string,item()*)
Params
- from as map(xs:string,item()*)
- to as map(xs:string,item()*)
- control as map(xs:string,item()*)
- d as xs:double
Returns
- map(xs:string,item()*)
declare function this:quad( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*), $d as xs:double ) as map(xs:string,item()*) { map { "kind": "quad", "u": $from, "v": $to, "c1": $control, "d": $d } }
Function: quad
declare function quad($from as map(xs:string,item()*),
$to as map(xs:string,item()*),
$control as map(xs:string,item()*)) as map(xs:string,item()*)
declare function quad($from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- from as map(xs:string,item()*)
- to as map(xs:string,item()*)
- control as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:quad( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*) ) as map(xs:string,item()*) { this:quad($from, $to, $control, 0) }
Function: controls
declare function controls($edge as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function controls($edge as map(xs:string,item()*)) as map(xs:string,item()*)*
controls()
Get control points from a quadratic or cubic edge.
Params
- edge as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:controls( $edge as map(xs:string,item()*) ) as map(xs:string,item()*)* { switch(this:kind($edge)) case "quad" return $edge("c1") case "cubic" return ($edge("c1"),$edge("c2")) default return errors:error("GEOM-BADREGION", ($edge, "controls")) }
Function: cubic
declare function cubic($from as map(xs:string,item()*),
$to as map(xs:string,item()*),
$control1 as map(xs:string,item()*),
$control2 as map(xs:string,item()*),
$d as xs:double,
$properties as map(xs:string,item()*)) as map(xs:string,item()*)
declare function cubic($from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*)) as map(xs:string,item()*)
cubic()
Construct a cubic edge.
Params
- from as map(xs:string,item()*): starting point
- to as map(xs:string,item()*): ending point
- control1 as map(xs:string,item()*): first control point
- control2 as map(xs:string,item()*): second control point
- d as xs:double: edge weight
- properties as map(xs:string,item()*): additional properties
Returns
- map(xs:string,item()*)
declare function this:cubic( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*) ) as map(xs:string,item()*) { util:merge-into($properties, map { "kind": "cubic", "u": $from, "v": $to, "c1": $control1, "c2": $control2, "d": $d } ) }
Function: cubic
declare function cubic($from as map(xs:string,item()*),
$to as map(xs:string,item()*),
$control1 as map(xs:string,item()*),
$control2 as map(xs:string,item()*),
$d as xs:double) as map(xs:string,item()*)
declare function cubic($from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*), $d as xs:double) as map(xs:string,item()*)
Params
- from as map(xs:string,item()*)
- to as map(xs:string,item()*)
- control1 as map(xs:string,item()*)
- control2 as map(xs:string,item()*)
- d as xs:double
Returns
- map(xs:string,item()*)
declare function this:cubic( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*), $d as xs:double ) as map(xs:string,item()*) { map { "kind": "cubic", "u": $from, "v": $to, "c1": $control1, "c2": $control2, "d": $d } }
Function: cubic
declare function cubic($from as map(xs:string,item()*),
$to as map(xs:string,item()*),
$control1 as map(xs:string,item()*),
$control2 as map(xs:string,item()*)) as map(xs:string,item()*)
declare function cubic($from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- from as map(xs:string,item()*)
- to as map(xs:string,item()*)
- control1 as map(xs:string,item()*)
- control2 as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:cubic( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*) ) as map(xs:string,item()*) { this:cubic($from, $to, $control1, $control2, 0) }
Function: points
declare function points($region as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function points($region as map(xs:string,item()*)) as map(xs:string,item()*)*
points()
Get 2D points from the region
Params
- region as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:points($region as map(xs:string,item()*)) as map(xs:string,item()*)* { this:vertices($region)!point:as-dimension(.,2) }
Function: vertices
declare function vertices($region as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function vertices($region as map(xs:string,item()*)) as map(xs:string,item()*)*
vertices()
Get all the region's points.
Params
- region as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:vertices($region as map(xs:string,item()*)) as map(xs:string,item()*)* { switch(this:kind($region)) case "edge" return ($region=>this:start(), $region=>this:end()) case "arc" return ($region=>this:start(), $region=>this:end()) case "quad" return ($region=>this:start(), $region=>this:end()) case "cubic" return ($region=>this:start(), $region=>this:end()) default return errors:error("GEOM-BADREGION", ($region, "vertices")) }
Function: to-edges
declare function to-edges($points as map(xs:string,item()*)*) as map(xs:string,item()*)*
declare function to-edges($points as map(xs:string,item()*)*) as map(xs:string,item()*)*
to-edges()
Convert a sequence of points into a sequence of edges.
Params
- points as map(xs:string,item()*)*: the sequence of points
Returns
- map(xs:string,item()*)*
declare function this:to-edges($points as map(xs:string,item()*)*) as map(xs:string,item()*)* { for $pt at $i in tail($points) return this:edge($points[$i], $pt) }
Function: to-edges
declare function to-edges($points as item()*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function to-edges($points as item()*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*
to-edges()
Convert a sequence of points into a sequence of edges.
Params
- points as item()*: the sequence of points
- properties as map(xs:string,item()*): edge properties
Returns
- map(xs:string,item()*)*
declare function this:to-edges($points as item()*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)* { let $d := ($properties("d"),0)[1] (: Special case for d :) for $pt at $i in tail($points) return this:edge($points[$i], $pt, $d, $properties) }
Function: skip
declare function skip($from as map(xs:string,item()*),
$to as map(xs:string,item()*)) as map(xs:string,item()*)
declare function skip($from as map(xs:string,item()*), $to as map(xs:string,item()*)) as map(xs:string,item()*)
skip()
Make a skip edge (pure goto)
Params
- from as map(xs:string,item()*): starting point
- to as map(xs:string,item()*): ending point
Returns
- map(xs:string,item()*)
declare function this:skip( $from as map(xs:string,item()*), $to as map(xs:string,item()*) ) as map(xs:string,item()*) { this:edge($from, $to)=>map:put("variety", "goto") }
Function: property-map
declare function property-map($region as map(xs:string,item()*)) as map(xs:string,item()*)
declare function property-map($region as map(xs:string,item()*)) as map(xs:string,item()*)
property-map()
Return the annotation properties of the edge as a map. Check whether this
is actually an edge.
Params
- region as map(xs:string,item()*): the region
Returns
- map(xs:string,item()*)
declare function this:property-map( $region as map(xs:string,item()*) ) as map(xs:string,item()*) { switch(this:kind($region)) case "edge" return util:exclude($region,$this:EDGE-RESERVED) case "arc" return util:exclude($region,$this:ARC-RESERVED) case "quad" return util:exclude($region, $this:QUAD-RESERVED) case "cubic" return util:exclude($region, $this:CUBIC-RESERVED) case "ellipse-arc" return util:exclude($region, $this:ELLIPSE-ARC-RESERVED) default return map {} }
Function: properties
declare function properties($region as map(xs:string,item()*)) as xs:string*
declare function properties($region as map(xs:string,item()*)) as xs:string*
properties()
Return the names of the annotation properties of the edge.
Check whether this is actually an edge
Params
- region as map(xs:string,item()*): the region
Returns
- xs:string*
declare function this:properties( $region as map(xs:string,item()*) ) as xs:string* { switch(this:kind($region)) case "edge" return ($region=>map:keys())[not(. = $this:EDGE-RESERVED)] case "arc" return ($region=>map:keys())[not(. = $this:ARC-RESERVED)] case "quad" return ($region=>map:keys())[not(. = $this:QUAD-RESERVED)] case "cubic" return ($region=>map:keys())[not(. = $this:CUBIC-RESERVED)] case "ellipse-arc" return ($region=>map:keys())[not(. = $this:ELLIPSE-ARC-RESERVED)] default return () }
Function: with-properties
declare function with-properties($region as map(xs:string,item()*),
$properties as map(xs:string,item()*)) as map(xs:string,item()*)
declare function with-properties($region as map(xs:string,item()*), $properties as map(xs:string,item()*)) as map(xs:string,item()*)
with-properties()
Annotate the edge with some new properties and return the new edge.
Will not touch any of the core properties. Will override existing properties
with the same keys but leave properties with different keys in place.
Raises an error if this is not actually an edge.
Params
- region as map(xs:string,item()*): the region
- properties as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:with-properties( $region as map(xs:string,item()*), $properties as map(xs:string,item()*) ) as map(xs:string,item()*) { switch(this:kind($region)) case "edge" return util:merge-into($region, util:exclude($properties,$this:EDGE-RESERVED)) case "arc" return util:merge-into($region, util:exclude($properties,$this:ARC-RESERVED)) case "quad" return util:merge-into($region, util:exclude($properties, $this:QUAD-RESERVED)) case "cubic" return util:merge-into($region, util:exclude($properties, $this:CUBIC-RESERVED)) case "ellipse-arc" return util:merge-into($region, util:exclude($properties,$this:ELLIPSE-ARC-RESERVED)) default return errors:error("GEOM-BADREGION", ($region, "with-properties")) }
Function: snap
declare function snap($edges as map(xs:string,item()*)*) as map(xs:string,item()*)*
declare function snap($edges as map(xs:string,item()*)*) as map(xs:string,item()*)*
snap()
Snap the coordinates of the points, returning the edges with snapped
(i.e. integer) points coordinates.
Params
- edges as map(xs:string,item()*)* the edges
Returns
- map(xs:string,item()*)*
declare function this:snap( $edges as map(xs:string,item()*)* ) as map(xs:string,item()*)* { for $edge in $edges return switch(this:kind($edge)) case "edge" return this:edge( point:snap(this:start($edge)), point:snap(this:end($edge)), this:weight($edge), $edge ) case "quad" return this:quad( point:snap(this:start($edge)), point:snap(this:end($edge)), point:snap(this:controls($edge)[1]), this:weight($edge), $edge ) case "cubic" return let $controls := this:controls($edge) return this:cubic( point:snap(this:start($edge)), point:snap(this:end($edge)), point:snap($controls[1]), point:snap($controls[2]), this:weight($edge), $edge ) case "arc" return let $circle := $edge=>this:arc-circle() let $points := $edge=>this:arc-ends() let $angles := $edge=>this:arc-angles() return if (empty($points)) then ( util:assert(exists($angles),"Arc must have points or angles"), $edge=> map:put("circle", ellipse:snap($circle)) ) else ( $edge=> map:put("circle", ellipse:snap($circle))=> map:put("start", point:snap($points[1]))=> map:put("end", point:snap($points[2])) ) case "ellipse-arc" return let $ellipse := $edge=>this:arc-ellipse() let $points := $edge=>this:arc-ends() return $edge=> map:put("ellipse", ellipse:snap($ellipse))=> map:put("start", point:snap($points[1]))=> map:put("end", point:snap($points[2])) default return $edge }
Function: decimal
declare function decimal($edges as map(xs:string,item()*)*,
$digits as xs:integer) as map(xs:string,item()*)*
declare function decimal($edges as map(xs:string,item()*)*, $digits as xs:integer) as map(xs:string,item()*)*
decimal()
Perform decimal rounding on all the point coordinates (see util:decimal).
Params
- edges as map(xs:string,item()*)*: the edges to round
- digits as xs:integer: how many digits after the decimal point to keep
Returns
- map(xs:string,item()*)*
declare function this:decimal( $edges as map(xs:string,item()*)*, $digits as xs:integer ) as map(xs:string,item()*)* { for $edge in $edges return switch(this:kind($edge)) case "edge" return this:edge( point:decimal(this:start($edge), $digits), point:decimal(this:end($edge), $digits), this:weight($edge), $edge ) case "quad" return this:quad( point:decimal(this:start($edge), $digits), point:decimal(this:end($edge), $digits), point:decimal(this:controls($edge)[1], $digits), this:weight($edge), $edge ) case "cubic" return let $controls := this:controls($edge) return this:cubic( point:decimal(this:start($edge), $digits), point:decimal(this:end($edge), $digits), point:decimal($controls[1], $digits), point:decimal($controls[2], $digits), this:weight($edge), $edge ) case "arc" return let $circle := $edge=>this:arc-circle() let $points := $edge=>this:arc-ends() let $angles := $edge=>this:arc-angles() return if (empty($points)) then ( util:assert(exists($angles),"Arc must have points or angles"), $edge=> map:put("circle", ellipse:decimal($circle, $digits)) ) else ( $edge=> map:put("circle", ellipse:decimal($circle, $digits))=> map:put("start", point:decimal($points[1], $digits))=> map:put("end", point:decimal($points[2], $digits)) ) case "ellipse-arc" return let $ellipse := $edge=>this:arc-ellipse() let $points := $edge=>this:arc-ends() return $edge=> map:put("ellipse", ellipse:decimal($ellipse, $digits))=> map:put("start", point:decimal($points[1], $digits))=> map:put("end", point:decimal($points[2], $digits)) default return $edge }
Function: quote
declare function quote($edges as map(xs:string,item()*)*) as xs:string
declare function quote($edges as map(xs:string,item()*)*) as xs:string
quote()
Return a string value for the edges, suitable for debugging.
Params
- edges as map(xs:string,item()*)*: the edge sequence to quote
Returns
- xs:string
declare function this:quote( $edges as map(xs:string,item()*)* ) as xs:string { string-join( for $edge in $edges return switch(this:kind($edge)) case "edge" return point:quote($edge("u"))||":"||point:quote($edge("v")) case "quad" return point:quote($edge("u"))||":"||point:quote($edge("v"))||"<"||point:quote($edge("c1"))||">" case "cubic" return point:quote($edge("u"))||":"||point:quote($edge("v"))||"<"||point:quote($edge("c1"))||";"||point:quote($edge("c2"))||">" case "arc" return "("|| ellipse:quote($edge("circle"))|| "<"|| point:quote($edge("start"))|| $edge("from-angle")||":"|| point:quote($edge("end"))|| $edge("to-angle")|| ">"|| (if ($edge("large")) then "+l1" else "+l0")|| (if ($edge("flipped")) then "+f1" else "+f0")|| ")" case "ellipse-arc" return "("|| ellipse:quote($edge("ellipse"))|| "<"|| point:quote($edge("start"))|| point:quote($edge("end"))|| ">"|| (if ($edge("large")) then "+l1" else "+l0")|| (if ($edge("flipped")) then "+f1" else "+f0")|| ")" default return errors:quote($edge) , " " ) }
Function: same
declare function same($this as map(xs:string,item()*),
$other as map(xs:string,item()*)) as xs:boolean
declare function same($this as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
same()
Equality comparison for edges, ignoring annotation properties.
Return true() if they have equal coordinates.
Params
- this as map(xs:string,item()*): one edge
- other as map(xs:string,item()*): the edge to compare it to
Returns
- xs:boolean
declare function this:same( $this as map(xs:string,item()*), $other as map(xs:string,item()*) ) as xs:boolean { let $this-kind := this:kind($this) let $other-kind := this:kind($other) return ( ($this-kind=$other-kind) and ( switch($this-kind) case "arc" return (this:arc-large($this) = this:arc-large($other)) and (this:arc-flipped($this) = this:arc-flipped($other)) and ellipse:same($this("circle"),$other("circle")) and ( if (empty($this("start"))) then empty($other("start")) else if (empty($other("start"))) then false() else point:same($this("start"), $other("start")) ) and ( if (empty($this("end"))) then empty($other("end")) else if (empty($other("end"))) then false() else point:same($this("end"), $other("end")) ) and ( if (empty($this("from-angle"))) then empty($other("from-angle")) else if (empty($other("from-angle"))) then false() else $this("from-angle")=$other("from-angle") ) and ( if (empty($this("to-angle"))) then empty($other("to-angle")) else if (empty($other("to-angle"))) then false() else $this("to-angle")=$other("to-angle") ) case "edge" return point:same($this("u"),$other("u")) and point:same($this("v"),$other("v")) case "quad" return point:same($this("u"),$other("u")) and point:same($this("v"),$other("v")) and point:same($this("c1"),$other("c1")) case "cubic" return point:same($this("u"),$other("u")) and point:same($this("v"),$other("v")) and point:same($this("c1"),$other("c1")) and point:same($this("c2"),$other("c2")) case "ellipse-arc" return (this:arc-large($this) = this:arc-large($other)) and (this:arc-flipped($this) = this:arc-flipped($other)) and ellipse:same($this("ellipse"),$other("ellipse")) and point:same($this("start"), $other("start")) and point:same($this("end"), $other("end")) default return deep-equal($this,$other) ) ) }
Function: mutate
declare function mutate($regions as map(xs:string,item()*)*,
$mutate as function(map(xs:string,item()*)) as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function mutate($regions as map(xs:string,item()*)*, $mutate as function(map(xs:string,item()*)) as map(xs:string,item()*)) as map(xs:string,item()*)*
mutate()
Run a function over a sequence of edges to produce a new sequence of
edges. The function maps points to points.
Params
- regions as map(xs:string,item()*)*: input sequence of edges
- mutate as function(map(xs:string,item()*))asmap(xs:string,item()*): function that takes a point as an argument and returns a new point
Returns
- map(xs:string,item()*)*
declare function this:mutate( $regions as map(xs:string,item()*)*, $mutate as function(map(xs:string,item()*)) as map(xs:string,item()*) (: point to point :) ) as map(xs:string,item()*)* { for $edge in $regions return ( switch (this:kind($edge)) case "edge" return ( this:edge( $mutate($edge=>this:start()), $mutate($edge=>this:end()), $edge=>this:weight(), $edge ) ) case "cubic" return ( let $controls := ($edge=>this:controls())!$mutate(.) return this:cubic( $mutate($edge=>this:start()), $mutate($edge=>this:end()), $controls[1], $controls[2], $edge=>this:weight(), $edge ) ) case "quad" return ( let $control := $mutate(this:controls($edge)[1]) return this:quad( $mutate($edge=>this:start()), $mutate($edge=>this:end()), $control, $edge=>this:weight(), $edge ) ) case "arc" return ( let $points := $edge=>this:arc-ends() let $angles := $edge=>this:arc-angles() return ( if (exists($points)) then ( this:arc( $mutate($edge=>this:arc-center()), $edge=>this:arc-radius(), $mutate($edge=>this:start()), $mutate($edge=>this:end()), $edge=>this:arc-flipped(), $edge=>this:arc-large() ) ) else ( this:arc-by-angle( $mutate($edge=>this:arc-center()), $edge=>this:arc-radius(), $angles[1], $angles[2], $edge=>this:arc-flipped(), $edge=>this:arc-large() ) ) )=>this:with-properties($edge) ) default return $edge ) }
Function: reverse
declare function reverse($edge as map(xs:string,item()*)) as map(xs:string,item()*)
declare function reverse($edge as map(xs:string,item()*)) as map(xs:string,item()*)
reverse()
Reverse the direction of the edge.
Params
- edge as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:reverse($edge as map(xs:string,item()*)) as map(xs:string,item()*) { switch(this:kind($edge)) case "edge" return this:edge( this:end($edge), this:start($edge), this:weight($edge), $edge ) case "quad" return this:quad( this:end($edge), this:start($edge), this:controls($edge)[1], this:weight($edge), $edge ) case "cubic" return this:cubic( this:end($edge), this:start($edge), this:controls($edge)[2], this:controls($edge)[1], this:weight($edge), $edge ) case "arc" return ( let $circle := $edge=>this:arc-circle() let $points := $edge=>this:arc-ends() let $angles := $edge=>this:arc-angles() return ( if (exists($points)) then ( this:arc( $circle=>ellipse:center(), $circle=>ellipse:radius(), $points[2], $points[1], not($edge=>this:arc-flipped()), $edge=>this:arc-large() ) ) else ( this:arc-by-angle( $circle=>ellipse:center(), $circle=>ellipse:radius(), $angles[2], $angles[1], not($edge=>this:arc-flipped()), $edge=>this:arc-large() ) ) ) ) default return $edge }
Function: angle
declare function angle($edge as map(xs:string,item()*)) as xs:double
declare function angle($edge as map(xs:string,item()*)) as xs:double
angle()
Bearing of the edge between start and end points.
Params
- edge as map(xs:string,item()*): the edge
Returns
- xs:double
declare function this:angle($edge as map(xs:string,item()*)) as xs:double { this:angle(this:start($edge), this:end($edge)) }
Function: angle
declare function angle($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double
declare function angle($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double
angle()
Compute the angle (azimuth) from one point to the next, in degrees
Return 0 if points are the same
Params
- last as map(xs:string,item()*)?: previous point; use (0,0) if no previous
- curr as map(xs:string,item()*): current point
Returns
- xs:double
declare function this:angle($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double { let $this := $curr=>point:as-dimension(2) let $prev := point:as-dimension(($last,$point:ORIGIN)[1],2) return if (point:same($prev,$this) or (point:distance($prev,$this) < $config:ε)) then 0 else if (point:px($this)=point:px($prev)) then ( if (point:py($prev) > point:py($this)) then 270 (: remapped -90 :) else 90 ) else ( util:remap-degrees(util:degrees( (math:pi() div 2) - math:atan2(point:px($this) - point:px($prev), point:py($this) - point:py($prev)) )) ) }
Function: inclination
declare function inclination($edge as map(xs:string,item()*)) as xs:double
declare function inclination($edge as map(xs:string,item()*)) as xs:double
inclination()
Inclination of the edge between start and end points.
Params
- edge as map(xs:string,item()*): the edge
Returns
- xs:double
declare function this:inclination($edge as map(xs:string,item()*)) as xs:double { this:inclination(this:start($edge), this:end($edge)) }
Function: inclination
declare function inclination($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double
declare function inclination($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double
inclination()
Compute the inclination angle from one point in the next, in degrees
Return 90 if the points are the same
Params
- last as map(xs:string,item()*)?: previous point; use (0,0,0) if no previous
- curr as map(xs:string,item()*): current point
Returns
- xs:double
declare function this:inclination($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double { let $curr := $curr=>point:as-dimension(3) let $prev := (($last,$point:ORIGIN3D)[1])=>point:as-dimension(3) let $d := point:distance($prev,$curr) return if (point:same($prev,$curr) or ($d < $config:ε)) then 90 else ( util:remap-degrees(util:degrees( math:acos( (point:pz($curr) - point:pz($prev)) div $d ) )) ) }
Function: linear-point
declare function linear-point($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
declare function linear-point($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
linear-point()
Compute the point on the edge the given fraction from the start
Params
- edge as map(xs:string,item()*): the edge
- t as xs:double: fraction of distance from start to end (t) [0,1]
Returns
- map(xs:string,item()*)
declare function this:linear-point($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { let $from := $edge=>this:start() let $to := $edge=>this:end() return ( point:map2(function ($a as xs:double, $b as xs:double) as xs:double {$a + ($b - $a)*$t}, $from, $to) ) }
Function: quad-point
declare function quad-point($quadratic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
declare function quad-point($quadratic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
quad-point()
Compute the point on the quad edge the given fraction from the start
Midpoint of quad edge is quad-point(0.5)
B(t)=(1-t)²P0 + 2(1-t)tP1 + t²P2, 0 <= t <= 1
Params
- quadratic as map(xs:string,item()*): the edge
- t as xs:double: fraction of distance from start to end (t) [0,1]
Returns
- map(xs:string,item()*)
declare function this:quad-point($quadratic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { let $from := $quadratic=>this:start() let $to := $quadratic=>this:end() let $controls := $quadratic=>this:controls() return ( point:map3( function ($a as xs:double, $b as xs:double, $c as xs:double) as xs:double { (1 - $t)*(1 - $t)*$a + 2*(1 - $t)*$t*$c + $t * $t * $b }, $from, $to, $controls[1] ) ) }
Function: cubic-point
declare function cubic-point($cubic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
declare function cubic-point($cubic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
cubic-point()
Compute the point on the cubic edge the given fraction from the start
Midpoint of cubic edge is cubic-point(0.5)
B(t)=(1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3, 0 <= t <= 1
Params
- cubic as map(xs:string,item()*): the edge
- t as xs:double: fraction of distance from start to end (t) [0,1]
Returns
- map(xs:string,item()*)
declare function this:cubic-point($cubic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { let $from := $cubic=>this:start() let $to := $cubic=>this:end() let $controls := $cubic=>this:controls() return ( point:map4( function ($a as xs:double, $b as xs:double, $c as xs:double, $d as xs:double) as xs:double { (1 - $t)*(1 - $t)*(1 - $t)*$a + 3*(1 - $t)*(1 - $t)*$t*$c + 3*(1 - $t)*$t*$t*$d + $t*$t*$t*$b }, $from, $to, $controls[1], $controls[2] ) ) }
Function: arc-point
declare function arc-point($arc as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
declare function arc-point($arc as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
arc-point()
Compute the point on the arc edge the given fraction from the start
Midpoint of arc edge is arc-point(0.5)
Will normalize arc properly.
Params
- arc as map(xs:string,item()*): the edge
- t as xs:double: fraction of distance from start to end (t) [0,1]
Returns
- map(xs:string,item()*)
declare function this:arc-point($arc as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $da := $to-angle - $from-angle return point:destination($center, $from-angle + $t*$da, $radius) }
Function: length
declare function length($edge as map(xs:string,item()*)) as xs:double
declare function length($edge as map(xs:string,item()*)) as xs:double
length()
Travel distance along edge.
Cubic is an approximation.
Params
- edge as map(xs:string,item()*)
Returns
- xs:double
declare function this:length($edge as map(xs:string,item()*)) as xs:double { switch (this:kind($edge)) case "arc" return this:arc-length($edge) case "quad" return this:quad-length($edge) case "cubic" return this:cubic-length($edge) (: approximation by 10 slices :) case "edge" return this:linear-length($edge) default return errors:error("GEOM-BADREGION", ($edge, "length")) }
Function: linear-length
declare function linear-length($edge as map(xs:string,item()*)) as xs:double
declare function linear-length($edge as map(xs:string,item()*)) as xs:double
linear-length()
Crow-flies distance between end points of an edge. Same a travel distance
along the edge if it is a linear edge.
Params
- edge as map(xs:string,item()*): the edge
Returns
- xs:double
declare function this:linear-length($edge as map(xs:string,item()*)) as xs:double { point:distance($edge=>this:start(), $edge=>this:end()) }
Function: quad-length
declare function quad-length($quad as map(xs:string,item()*)) as xs:double
declare function quad-length($quad as map(xs:string,item()*)) as xs:double
quad-length()
Length of travel along a quadratic edge.
Params
- quad as map(xs:string,item()*): the quadratic edge Derivation, see: https://members.loria.fr/SHornus/quadratic-arc-length.html B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2 Reformulate to: P(t) = At² + 2Bt + C where A = P0 - P1 + P2 - P1 B = P1 - P0 C = P0 let F = P2 - P1, so A = F - B length = (|F|(A · F) - |B|(A · B)) / |A|² + (|B|²/|A| - (A · B)²/|A|³) X (log(|A||F| + A · F) - log(|A||B| + A · B))
Returns
- xs:double
declare function this:quad-length($quad as map(xs:string,item()*)) as xs:double { let $P0 := this:start($quad) let $P1 := this:controls($quad)[1] let $P2 := this:end($quad) let $F := point:sub($P2, $P1) let $B := point:sub($P1, $P0) let $A := point:sub($F, $B) let $nA := point:magnitude($A) let $nB := point:magnitude($B) let $nF := point:magnitude($F) let $A.B := point:dot($A, $B) let $A.F := point:dot($A, $F) return ( (: Possible error cases : nA=0 => F=B => start, c0, end are colinear => a straight edge : : nA*nF + A.F = 0 => nA*nF = -A.F => log(nA*nF + A.F) = -INF : nA*nF= √(Σai*ai)√(Σfi*fi) A.F=Σ(ai*fi) : A = F - B => nA*nF = √(Σ(fi-bi)*(fi-bi))√(Σfi*fi) A.F=Σ((fi-bi)*fi) => det(B,F) = 0 => : B and F are colinear => a straight edge again : : nA*nB + A.B = 0 => log(nA*nA + A.B) = -INF => nA*nB = -A.B => nA*nA*nB*nB = A.B*A.B => : (nB*nB / nA) - (A.B*A.B)/(nA*nA*nA) = 0 => treat 0*-INF as 0 :) if ($nA*$nA = 0) then ( point:distance($P0, $P2) ) else if ($nA*$nB + $A.B = 0) then ( ($nF * $A.F - $nB * $A.B) div ($nA*$nA) ) else if ($nA * $nF + $A.F <= 0) then ( point:distance($P0, $P2) ) else if ($nA * $nB + $A.B <= 0) then ( point:distance($P0, $P2) ) else ( ( ($nF * $A.F - $nB * $A.B) div ($nA*$nA) ) + ( (($nB*$nB div $nA) - ($A.B*$A.B) div ($nA*$nA*$nA)) * (math:log($nA*$nF + $A.F) - math:log($nA*$nB + $A.B)) ) ) ) }
Function: cubic-length
declare function cubic-length($cubic as map(xs:string,item()*),
$n as xs:integer) as xs:double
declare function cubic-length($cubic as map(xs:string,item()*), $n as xs:integer) as xs:double
cubic-length()
Travel length along a cubic edge. There is no general closed form,
so this is approximated by interpolating the edge with line segments.
Params
- cubic as map(xs:string,item()*): the cubic edge
- n as xs:integer: number of interpolations to use
Returns
- xs:double
declare function this:cubic-length( $cubic as map(xs:string,item()*), $n as xs:integer ) as xs:double { let $ts := util:linspace($n, 0, 1) let $pts := for $t in $ts return this:cubic-point($cubic, $t) return sum( for $pt at $i in tail($pts) return point:distance($pts[$i], $pt) ) }
Function: cubic-length
declare function cubic-length($cubic as map(xs:string,item()*)) as xs:double
declare function cubic-length($cubic as map(xs:string,item()*)) as xs:double
cubic-length()
Travel length along a cubic edge. There is no general closed form,
so this is approximated by interpolating the edge with 10 line segments.
Params
- cubic as map(xs:string,item()*): the cubic edge
Returns
- xs:double
declare function this:cubic-length($cubic as map(xs:string,item()*)) as xs:double { this:cubic-length($cubic, 10) }
Function: arc-length
declare function arc-length($arc as map(xs:string,item()*)) as xs:double
declare function arc-length($arc as map(xs:string,item()*)) as xs:double
arc-length()
Travel distance along arc
Params
- arc as map(xs:string,item()*)
Returns
- xs:double
declare function this:arc-length($arc as map(xs:string,item()*)) as xs:double { let $r := $arc=>this:arc-radius() let $α := this:arc-extent($arc) return 2 * math:pi() * $r * $α div 360 }
Function: arc-extent
declare function arc-extent($arc as map(xs:string,item()*)) as xs:double
declare function arc-extent($arc as map(xs:string,item()*)) as xs:double
arc-extent()
Degrees swept out by an arc edge.
Params
- arc as map(xs:string,item()*): the arc edge
Returns
- xs:double
declare function this:arc-extent($arc as map(xs:string,item()*)) as xs:double { let $center := $arc=>this:arc-center() let $points := $arc=>this:arc-ends() let $angles := $arc=>this:arc-angles() return ( if (exists($points)) then ( util:remap-degrees(abs(this:angle($center, $points[2]) - this:angle($center, $points[1]))) ) else ( util:remap-degrees(abs($angles[2] - $angles[1])) ) ) }
Function: slice
declare function slice($edge as map(xs:string,item()*),
$t as xs:double) as map(xs:string,item()*)*
declare function slice($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)*
slice()
Create two edges at the given fraction of the source edge. Cubic edges
will be sliced into two cubic edges, quadratric edges will be slices into
two quadratic edges, etc.
If t is at end, get same edge back.
Params
- edge as map(xs:string,item()*): the edge to cut
- t as xs:double: fraction of edge [0,1]
Returns
- map(xs:string,item()*)*
declare function this:slice( $edge as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { switch (this:kind($edge)) case "edge" return this:slice-linear($edge, $t) case "arc" return this:slice-arc($edge, $t) case "quad" return this:slice-quad($edge, $t) case "cubic" return this:slice-cubic($edge, $t) default return errors:error("GEOM-BADREGION", ($edge, "slice")) }
Function: slice
declare function slice($edge as map(xs:string,item()*),
$start-t as xs:double,
$end-t as xs:double) as map(xs:string,item()*)*
declare function slice($edge as map(xs:string,item()*), $start-t as xs:double, $end-t as xs:double) as map(xs:string,item()*)*
slice()
Slice the edge between two cut points. Will end up with three edges in
general: [start, p(start-t)], [p(start-t), p(end-t)], [p(end-t), end]
start-t <= end-t
If start-t=end-t this is same as slice(edge, start-t)
If start-t or end-t is at an end will get one less edge
Cubics are cut into cubics, quads into quads, straight edges into straight
edges
Params
- edge as map(xs:string,item()*): the edge to cut
- start-t as xs:double: fraction of edge [0,1]
- end-t as xs:double: fraction of edge[0,1]
Returns
- map(xs:string,item()*)*
declare function this:slice( $edge as map(xs:string,item()*), $start-t as xs:double, $end-t as xs:double ) as map(xs:string,item()*)* { if ($start-t > $end-t) then errors:error("GEOM-BADT", ($start-t, $end-t)) else (), if ($start-t = $end-t) then this:slice($edge, $start-t) else if ($start-t = 1) then $edge (: start-t=end-t=1 :) else if ($start-t = 0) then this:slice($edge, $end-t) else ( (: : end-t needs to be adjusted for the second slice: : Effective end-t should be fraction of length(slice) not of length(edge) : e * l - l1 = x * l2; l1 = s * l, l2 = l - l1 = l - s * l : x = (el - sl) / (s - sl) = (e - s) / (1 - s) :) let $slice := this:slice($edge, $start-t) let $eff-end-t := ($end-t - $start-t) div (1 - $start-t) return ($slice[1], this:slice($slice[2], $eff-end-t)) ) }
Function: chop
declare function chop($edge as map(xs:string,item()*),
$ts as xs:double*) as map(xs:string,item()*)*
declare function chop($edge as map(xs:string,item()*), $ts as xs:double*) as map(xs:string,item()*)*
chop()
Slice the edge by chopping it at the given cut points.
Cubics are cut into cubics, quads into quads, straight edges into straight
edges
Params
- edge as map(xs:string,item()*): the edge to cut
- ts as xs:double*: fraction of edge [0,1]
Returns
- map(xs:string,item()*)*
declare function this:chop( $edge as map(xs:string,item()*), $ts as xs:double* ) as map(xs:string,item()*)* { let $ts := distinct-values(for $t in $ts order by $t ascending return $t) let $eff-ts := (: Adjust ts: : Need to adjust t2 : Effective t2 should be fraction of length(slice) : e * l - l1 = x * l2; l1 = s * l, l2 = l - l1 = l - s * l : x = (el - sl) / (s - sl) = (e - s) / (1 - s) : However, the t1 is itself an adjustment : So t4 needs to be adjusted to t3 which is adjusted to t2 : which is adjusted to t1 and so on. But we need to adjust : them all for each go round: : [ t1 t2 t3 ] : [ t1][ t2' t3'] : [ t1][ t2'][ t3''] :) fold-left(2 to count($ts) - 1, $ts, function ($new-ts as xs:double*, $it as xs:integer) as xs:double* { $new-ts[position() < $it - 1], fold-left($new-ts[position() >= $it], $new-ts[position() = $it - 1], function ($new-ts as xs:double*, $t2 as xs:double) as xs:double* { $new-ts, let $t1 := $new-ts[last()] return ($t2 - $t1) div (1 - $t1) } ) } ) let $split1 := this:slice($edge, head($eff-ts)) let $splits := ( fold-left(tail($eff-ts), array{$split1}, function ($splits as array(*)*, $t as xs:double) as array(*)* { $splits, array { if (empty($splits[last()]) or array:size($splits[last()]) < 2) then () else this:slice($splits[last()](2), $t) } } ) ) return ( for $split in $splits return $split?* ) }
Function: slice-linear
declare function slice-linear($edge as map(xs:string,item()*),
$t as xs:double) as map(xs:string,item()*)*
declare function slice-linear($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)*
slice-linear()
Create two straight edges at the given fraction of the source edge.
If t is at end, get same edge back.
Params
- edge as map(xs:string,item()*)
- t as xs:doublehis: the straight edge to cut: fraction of edge [0,1]
Returns
- map(xs:string,item()*)*
declare function this:slice-linear( $edge as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { if ($t = (0,1)) then $edge else let $p1 := this:start($edge) let $p2 := this:end($edge) let $pt := this:linear-point($edge, $t) return ( this:edge($p1, $pt), this:edge($pt, $p2) ) }
Function: slice-quad
declare function slice-quad($quad as map(xs:string,item()*),
$t as xs:double) as map(xs:string,item()*)*
declare function slice-quad($quad as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)*
slice-quad()
Create two quadratic edges at the given fraction of the source quadratic.
If t is at end, get same edge back.
Params
- quad as map(xs:string,item()*): the quad edge to cut
- t as xs:double: fraction of edge [0,1]
Returns
- map(xs:string,item()*)*
declare function this:slice-quad( $quad as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { if ($t = (0,1)) then $quad else let $p1 := this:start($quad) let $p2 := this:controls($quad)[1] let $p3 := this:end($quad) let $p12 := point:add($p1, point:sub($p2, $p1)=>point:times($t)) let $p23 := point:add($p2, point:sub($p3, $p2)=>point:times($t)) let $p123 := point:add($p12, point:sub($p23, $p12)=>point:times($t)) return ( (: Avoid creating quads that are really lines :) if (point:same($p1, $p2, $config:ε) or point:same($p2, $p3, $config:ε)) then this:slice-linear(this:edge($p1, $p3), $t) else ( if (point:same($p1, $p12, $config:ε) or point:same($p12, $p123, $config:ε)) then ( this:edge($p1, $p123) ) else ( this:quad($p1, $p123, $p12) ), if (point:same($p123, $p23, $config:ε) or point:same($p23, $p3, $config:ε)) then ( this:edge($p123, $p3) ) else ( this:quad($p123, $p3, $p23) ) ) ) }
Function: slice-cubic
declare function slice-cubic($cubic as map(xs:string,item()*),
$t as xs:double) as map(xs:string,item()*)*
declare function slice-cubic($cubic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)*
slice-cubic()
Create two cubic edges at the given fraction of the source cubic.
If t is at end, get same edge back.
Params
- cubic as map(xs:string,item()*): the cubic edge to cut
- t as xs:double: fraction of edge [0,1]
Returns
- map(xs:string,item()*)*
declare function this:slice-cubic( $cubic as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { if ($t = (0,1)) then $cubic else let $p1 := this:start($cubic) let $p2 := this:controls($cubic)[1] let $p3 := this:controls($cubic)[2] let $p4 := this:end($cubic) let $p12 := point:add($p1, point:sub($p2, $p1)=>point:times($t)) let $p23 := point:add($p2, point:sub($p3, $p2)=>point:times($t)) let $p34 := point:add($p3, point:sub($p4, $p3)=>point:times($t)) let $p123 := point:add($p12, point:sub($p23, $p12)=>point:times($t)) let $p234 := point:add($p23, point:sub($p34, $p23)=>point:times($t)) let $p1234 := point:add($p123, point:sub($p234, $p123)=>point:times($t)) return ( if (point:same($p1, $p2, $config:ε) or point:same($p2, $p3, $config:ε)) then this:slice-quad(this:quad($p1, $p3, $p4), $t) else if (point:same($p4, $p3, $config:ε)) then this:slice-quad(this:quad($p1, $p2, $p4), $t) else ( (: Avoid creating "cubics" that are really quads or lines :) if (point:same($p12, $p123, $config:ε)) then ( if (point:same($p1, $p12, $config:ε) or point:same($p123, $p1234, $config:ε)) then ( (: Really a line: make a straight edge :) this:edge($p1, $p1234) ) else ( (: Really a quad: make a quad edge :) this:quad($p1, $p1234, $p12) ) ) else ( this:cubic($p1, $p1234, $p12, $p123) ), if (point:same($p234, $p34, $config:ε)) then ( if (point:same($p1234, $p234, $config:ε) or point:same($p34, $p4, $config:ε)) then ( (: Really a line: make a straight edge :) this:edge($p1234, $p4) ) else ( (: Really a quad: make a quad edge :) this:quad($p1234, $p4, $p234) ) ) else ( this:cubic($p1234, $p4, $p234, $p34) ) ) ) }
Function: slice-arc
declare function slice-arc($arc as map(xs:string,item()*),
$t as xs:double) as map(xs:string,item()*)*
declare function slice-arc($arc as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)*
slice-arc()
Create two arc edges at the given fraction of the source arc.
If t is at end, get same edge back.
Normalizes the arc
Params
- arc as map(xs:string,item()*): the arc edge to cut
- t as xs:double: fraction of edge [0,1]
Returns
- map(xs:string,item()*)*
declare function this:slice-arc( $arc as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { if ($t = (0,1)) then $arc else let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $da := $to-angle - $from-angle let $at := $from-angle + $t*$da let $larges := if ($arc=>this:arc-large()) then ( util:remap-degrees($at - $from-angle) > 180, util:remap-degrees($to-angle - $at) > 180 ) else ( false(), false() ) let $a1 := this:arc-by-angle($center, $radius, util:remap-degrees($from-angle), util:remap-degrees($at), false(), $larges[1]) let $a2 := this:arc-by-angle($center, $radius, util:remap-degrees($at), util:remap-degrees($to-angle), false(), $larges[2]) return ( if (point:same(this:start($arc), this:end($arc), $config:ε) and not(this:arc-large($arc))) then ( this:slice-linear(this:edge(this:start($arc), this:end($arc)), $t) ) else ( if (not($larges[1]) and point:same(this:start($a1), this:end($a1), $config:ε)) then ( this:edge(this:start($a1), this:end($a1)) ) else ( $a1 ), if (not($larges[2]) and point:same(this:start($a2), this:end($a2), $config:ε)) then ( this:edge(this:start($a2), this:end($a2)) ) else ( $a2 ) ) ) }
Function: midpoint
declare function midpoint($a as map(xs:string,item()*), $b as map(xs:string,item()*)) as map(xs:string,item()*)
declare function midpoint($a as map(xs:string,item()*), $b as map(xs:string,item()*)) as map(xs:string,item()*)
midpoint()
Return midpoint between two points
Params
- a as map(xs:string,item()*): one point
- b as map(xs:string,item()*): the other
Returns
- map(xs:string,item()*)
declare function this:midpoint($a as map(xs:string,item()*), $b as map(xs:string,item()*)) as map(xs:string,item()*) { point:midpoint($a, $b) }
Function: midpoint
declare function midpoint($edge as map(xs:string,item()*)) as map(xs:string,item()*)
declare function midpoint($edge as map(xs:string,item()*)) as map(xs:string,item()*)
midpoint()
Center of the edge
Note: if you use on an arc, the arc must be normalized (edge:normalize-arc)
Params
- edge as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:midpoint($edge as map(xs:string,item()*)) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return point:midpoint(this:start($edge), this:end($edge)) case "quad" return this:quad-point($edge, 0.5) case "cubic" return this:cubic-point($edge, 0.5) case "arc" return this:arc-point($edge, 0.5) default return errors:error("GEOM-BADREGION", ($edge, "midpoint")) }
Function: arc-midpoint
declare function arc-midpoint($center as map(xs:string,item()*),
$a as map(xs:string,item()*),
$b as map(xs:string,item()*)) as map(xs:string,item()*)
declare function arc-midpoint($center as map(xs:string,item()*), $a as map(xs:string,item()*), $b as map(xs:string,item()*)) as map(xs:string,item()*)
arc-midpoint()
Center of an arc. Note: arcs are treated as 2D, so output is 2D point;
we only look at x and y coordinates in the calculations.
Params
- center as map(xs:string,item()*): center of the circle defining the arc
- a as map(xs:string,item()*): start of arc
- b as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:arc-midpoint( $center as map(xs:string,item()*), $a as map(xs:string,item()*), $b as map(xs:string,item()*) ) as map(xs:string,item()*) { let $r := point:distance($center, $a) let $lm := this:midpoint($a, $b) let $v := point:sub($lm, $center) let $lenv := math:sqrt(point:px($v)*point:px($v) + point:py($v)*point:py($v)) return if (point:sorientation($a, $b, $center) > 0) then ( point:sub( $center, point:point($r*point:px($v) div $lenv, $r*point:py($v) div $lenv) ) ) else ( point:add( $center, point:point($r*point:px($v) div $lenv, $r*point:py($v) div $lenv) ) ) }
Function: edge-point
declare function edge-point($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
declare function edge-point($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
edge-point()
Compute the point on the edge the given fraction from the start
Params
- edge as map(xs:string,item()*): the edge
- t as xs:double: fraction of distance from start to end (t) [0,1]
Returns
- map(xs:string,item()*)
declare function this:edge-point($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return this:linear-point($edge, $t) case "arc" return this:arc-point($edge, $t) case "quad" return this:quad-point($edge, $t) case "cubic" return this:cubic-point($edge, $t) default return errors:error("GEOM-BADREGION", ($edge, "edge-point")) }
Function: point-distance
declare function point-distance($edge as map(xs:string,item()*),
$pt as map(xs:string,item()*)) as xs:double
declare function point-distance($edge as map(xs:string,item()*), $pt as map(xs:string,item()*)) as xs:double
point-distance()
Shortest distance from point to linear edge
Params
- edge as map(xs:string,item()*)
- pt as map(xs:string,item()*)
Returns
- xs:double
declare function this:point-distance( $edge as map(xs:string,item()*), $pt as map(xs:string,item()*) ) as xs:double { if (point:same(this:start($edge), this:end($edge))) then ( point:distance($pt, this:start($edge)) ) else ( let $v := this:start($edge) let $w := this:end($edge) let $l := this:linear-length($edge) let $lsquared := $l*$l let $w_sub_v := point:sub($w, $v) let $t := max((0, min((1, point:dot(point:sub($pt, $v), $w_sub_v) div $lsquared)))) return ( point:distance($pt, point:point( point:px($v) + $t * point:px($w_sub_v), point:py($v) + $t * point:py($w_sub_v) ) ) ) ) }
Function: closest-point
declare function closest-point($edge as map(xs:string,item()*),
$pt as map(xs:string,item()*)) as map(xs:string,item()*)
declare function closest-point($edge as map(xs:string,item()*), $pt as map(xs:string,item()*)) as map(xs:string,item()*)
closest-point()
Point closest to the target point on the (linear) edge
Params
- edge as map(xs:string,item()*)
- pt as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:closest-point( $edge as map(xs:string,item()*), $pt as map(xs:string,item()*) ) as map(xs:string,item()*) { let $start := this:start($edge) let $end := this:end($edge) return ( if (point:px($start)=point:px($end)) then point:point(point:px($start), point:py($pt)) else if (point:py($start)=point:py($end)) then point:point(point:px($pt), point:py($start)) else ( let $m1 := (point:py($end) - point:py($start)) div (point:px($end) - point:px($start)) let $m2 := -1 div $m1 let $x := ($m1*point:px($start) - $m2*point:px($pt) + point:py($pt) - point:py($start)) div ($m1 - $m2) let $y := $m2*($x - point:px($pt)) + point:py($pt) return ( point:point($x, $y) ) ) ) }
Function: on-segment-colinear
declare function on-segment-colinear($edge as map(xs:string,item()*),
$p as map(xs:string,item()*),
$tolerance as xs:double) as xs:boolean
declare function on-segment-colinear($edge as map(xs:string,item()*), $p as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
on-segment-colinear()
Given edge and colinear point, check whether point is on that edge
Params
- edge as map(xs:string,item()*)
- p as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:on-segment-colinear( $edge as map(xs:string,item()*), $p as map(xs:string,item()*), $tolerance as xs:double ) as xs:boolean { util:twixt(point:px($p), min(this:vertices($edge)!point:px(.)) - $tolerance, max(this:vertices($edge)!point:px(.)) + $tolerance ) and util:twixt(point:py($p), min(this:vertices($edge)!point:py(.)) - $tolerance, max(this:vertices($edge)!point:py(.)) + $tolerance ) }
Function: on-segment-colinear
declare function on-segment-colinear($edge as map(xs:string,item()*),
$p as map(xs:string,item()*)) as xs:boolean
declare function on-segment-colinear($edge as map(xs:string,item()*), $p as map(xs:string,item()*)) as xs:boolean
Params
- edge as map(xs:string,item()*)
- p as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:on-segment-colinear( $edge as map(xs:string,item()*), $p as map(xs:string,item()*) ) as xs:boolean { this:on-segment-colinear($edge, $p, $config:tolerance) }
Function: on-segment
declare function on-segment($edge as map(xs:string,item()*),
$p as map(xs:string,item()*)) as xs:boolean
declare function on-segment($edge as map(xs:string,item()*), $p as map(xs:string,item()*)) as xs:boolean
on-segment()
Given edge and arbitary point, check whether point is on that edge
Params
- edge as map(xs:string,item()*)
- p as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:on-segment( $edge as map(xs:string,item()*), $p as map(xs:string,item()*) ) as xs:boolean { this:on-segment($edge, $p, $config:tolerance) }
Function: on-segment
declare function on-segment($edge as map(xs:string,item()*),
$p as map(xs:string,item()*),
$tolerance as xs:double) as xs:boolean
declare function on-segment($edge as map(xs:string,item()*), $p as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
Params
- edge as map(xs:string,item()*)
- p as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:on-segment( $edge as map(xs:string,item()*), $p as map(xs:string,item()*), $tolerance as xs:double ) as xs:boolean { ( (point:sorientation(this:start($edge), this:end($edge), $p)=0) or (this:point-distance($edge, $p) < $tolerance) ) and ( util:twixt(point:px($p), min(this:vertices($edge)!point:px(.)) - $tolerance, max(this:vertices($edge)!point:px(.)) + $tolerance ) ) and ( util:twixt(point:py($p), min(this:vertices($edge)!point:py(.)) - $tolerance, max(this:vertices($edge)!point:py(.)) + $tolerance ) ) }
Function: linear-t
declare function linear-t($edge as map(xs:string,item()*),
$point as map(xs:string,item()*)) as xs:double*
declare function linear-t($edge as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double*
linear-t()
Compute the t value of the point wrt the linear edge.
Params
- edge as map(xs:string,item()*)
- point as map(xs:string,item()*)
Returns
- xs:double*
declare function this:linear-t( $edge as map(xs:string,item()*), $point as map(xs:string,item()*) ) as xs:double* { if (this:on-segment-colinear($edge, $point)) then ( point:distance(this:start($edge), $point) div this:length($edge) ) else () }
Function: on-quad
declare function on-quad($quad as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
declare function on-quad($quad as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
on-quad()
True if the point is on the quadractic edge.
B(t)=(1-t)²P0 + 2(1-t)tP1 + t²P2, 0 <= t <= 1
B(t)=(P0 - 2P1 + P2)t² + (2P1 - 2P0)t + P0
If Q is on quad then solve (P0 - 2P1 + P2)t² + (2P1 - 2P0)t + P0 - Q = 0
i.e. (-b ± √((b² - 4ac))/2a where a=(P0 - 2P1 + P2), b=(2P1 - 2P0), c = (P0 - Q)
If t in range then you're good
We're using equality within ε here, because floating point equality is
problematic. Alternatively: you could compute minimum distance and
make sure it is within tolerance.
Params
- quad as map(xs:string,item()*)
- point as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:on-quad($quad as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { exists(this:quad-t($quad, $point, $tolerance)) }
Function: on-quad
declare function on-quad($quad as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean
declare function on-quad($quad as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean
Params
- quad as map(xs:string,item()*)
- point as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:on-quad($quad as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean { this:on-quad($quad, $point, $config:tolerance) }
Function: quad-t
declare function quad-t($quad as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:double*
declare function quad-t($quad as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:double*
quad-t()
Compute the t value of the point wrt the quadractic edge.
B(t)=(1-t)²P0 + 2(1-t)tP1 + t²P2, 0 <= t <= 1
B(t)=(P0 - 2P1 + P2)t² + (2P1 - 2P0)t + P0
If Q is on quad then solve (P0 - 2P1 + P2)t² + (2P1 - 2P0)t + P0 - Q = 0
i.e. (-b ± √((b² - 4ac))/2a where a=(P0 - 2P1 + P2), b=(2P1 - 2P0), c = (P0 - Q)
If t in range then you're good
We're using equality within ε here, because floating point equality is
problematic. Alternatively: you could compute minimum distance and
make sure it is within tolerance.
Params
- quad as map(xs:string,item()*)
- point as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:double*
declare function this:quad-t($quad as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:double* { if (point:same($point, this:start($quad), $tolerance)) then 0.0 else if (point:same($point, this:end($quad), $tolerance)) then 1.0 else ( let $P0 := this:start($quad) let $P1 := this:controls($quad)[1] let $P2 := this:end($quad) let $ax := point:px($P0) - 2*point:px($P1) + point:px($P2) let $bx := 2*point:px($P1) - 2*point:px($P0) let $cx := point:px($P0) - point:px($point) let $x-all-zero := every $x in ($ax, $bx, $cx) satisfies abs($x) < $config:ε let $ay := point:py($P0) - 2*point:py($P1) + point:py($P2) let $by := 2*point:py($P1) - 2*point:py($P0) let $cy := point:py($P0) - point:py($point) let $y-all-zero := every $y in ($ay, $by, $cy) satisfies abs($y) < $config:ε return ( if ($x-all-zero and $y-all-zero) then ( (: : Quad is really a point : If this works it was caught above :) ) else if ($x-all-zero) then ( (: : Quad is really a straight line with all x's the same : If this works point x must be same, then use ty :) if (abs(point:px($point) - point:px($P0)) < $tolerance) then ( let $ty := roots:quadratic-real-roots($ay, $by, $cy)[util:twixt(., -$config:ε, 1 + $config:ε)] for $t in $ty return util:clamp($t, 0.0, 1.0) ) else ( ) ) else if ($y-all-zero) then ( (: : Quad is really a straight line with all y's the same : If this works point y must be same, then use tx :) if (abs(point:py($point) - point:py($P0)) < $tolerance) then ( let $tx := roots:quadratic-real-roots($ax, $bx, $cx)[util:twixt(., -$config:ε, 1 + $config:ε)] for $t in $tx return util:clamp($t, 0.0, 1.0) ) else ( ) ) else ( (: : Normal case: match tx with ty :) let $tx := roots:quadratic-real-roots($ax, $bx, $cx)[util:twixt(., -$config:ε, 1 + $config:ε)] let $ty := roots:quadratic-real-roots($ay, $by, $cy)[util:twixt(., -$config:ε, 1 + $config:ε)] let $ts := $tx[some $t in $ty satisfies abs(. - $t) < $config:ε] for $t in $ts return util:clamp($t, 0.0, 1.0) ) ) ) }
Function: quad-t
declare function quad-t($quad as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double*
declare function quad-t($quad as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double*
Params
- quad as map(xs:string,item()*)
- point as map(xs:string,item()*)
Returns
- xs:double*
declare function this:quad-t($quad as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double* { this:quad-t($quad, $point, $config:tolerance) }
Function: on-cubic
declare function on-cubic($cubic as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
declare function on-cubic($cubic as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
on-cubic()
True if the point is on the cubic edge.
B(t)=(1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3, 0 <= t <= 1
B(t)=(P3 - 3P2 + P1 - P0)t³ + (3P2 - 6P1)t² + (3P1)t + P0
If Q is on cubic then solve
(P3 - 3P2 + P1 - P0)t³ + (3P2 - 6P1)t² + (3P1)t + P0 - Q = 0
i.e. cubic roots
If t in range then you're good
We're using equality within ε here, because floating point equality is
problematic. Alternatively: you could compute minimum distance and
make sure it is within tolerance.
Params
- cubic as map(xs:string,item()*)
- point as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:on-cubic($cubic as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { exists(this:cubic-t($cubic, $point, $tolerance)) }
Function: on-cubic
declare function on-cubic($cubic as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean
declare function on-cubic($cubic as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean
Params
- cubic as map(xs:string,item()*)
- point as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:on-cubic($cubic as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean { this:on-cubic($cubic, $point, $config:tolerance) }
Function: cubic-t
declare function cubic-t($cubic as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:double*
declare function cubic-t($cubic as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:double*
cubic-t()
Compute the t value of the point wrt the cubic edge.
B(t)=(1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3, 0 <= t <= 1
B(t)=(-P0 + 3P1 - 3P2 + P3)t³ + (3P0 - 6P1 + 3P2)t² + (-3P0 + 3P1)t + P0
If Q is on cubic then solve
(-P0 + 3P1 - 3P2 + P3)t³ + (3P0 - 6P1 + 3P2)t² + (-3P0 + 3P1)t + (P0 - Q)
i.e. cubic roots
If t in range then you're good
We're using equality within ε here, because floating point equality is
problematic. Alternatively: you could compute minimum distance and
make sure it is within tolerance.
Params
- cubic as map(xs:string,item()*)
- point as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:double*
declare function this:cubic-t($cubic as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:double* { if (point:same($point, this:start($cubic), $tolerance)) then 0.0 else if (point:same($point, this:end($cubic), $tolerance)) then 1.0 else ( let $P0 := this:start($cubic) let $P1 := this:controls($cubic)[1] let $P2 := this:controls($cubic)[2] let $P3 := this:end($cubic) let $ax := -point:px($P0) + 3*point:px($P1) - 3*point:px($P2) + point:px($P3) let $bx := 3*point:px($P0) - 6*point:px($P1) + 3*point:px($P2) let $cx := -3*point:px($P0) + 3*point:px($P1) let $dx := point:px($P0) - point:px($point) let $x-all-zero := every $x in ($ax, $bx, $cx, $dx) satisfies abs($x) < $config:ε let $ay := -point:py($P0) + 3*point:py($P1) - 3*point:py($P2) + point:py($P3) let $by := 3*point:py($P0) - 6*point:py($P1) + 3*point:py($P2) let $cy := -3*point:py($P0) + 3*point:py($P1) let $dy := point:py($P0) - point:py($point) let $y-all-zero := every $y in ($ay, $by, $cy, $dy) satisfies abs($y) < $config:ε return ( if ($x-all-zero and $y-all-zero) then ( (: : Cubic is really a point : If this works it was caught above :) ) else if ($x-all-zero) then ( (: : Cubic is really a straight line with all x's the same : If this works point x must be same, then use ty :) if (abs(point:px($point) - point:px($P0)) < $tolerance) then ( let $ty := roots:cubic-real-roots($ay, $by, $cy, $dy)[util:twixt(., -$config:ε, 1 + $config:ε)] for $t in $ty return util:clamp($t, 0.0, 1.0) ) else ( ) ) else if ($y-all-zero) then ( (: : Cubic is really a straight line with all y's the same : If this works point y must be same, then use tx :) if (abs(point:py($point) - point:py($P0)) < $tolerance) then ( let $tx := roots:cubic-real-roots($ax, $bx, $cx, $dx)[util:twixt(., -$config:ε, 1 + $config:ε)] for $t in $tx return util:clamp($t, 0.0, 1.0) ) else ( ) ) else ( (: : Normal case: match tx with ty :) let $tx := roots:cubic-real-roots($ax, $bx, $cx, $dx)[util:twixt(., -$config:ε, 1 + $config:ε)] let $ty := roots:cubic-real-roots($ay, $by, $cy, $dy)[util:twixt(., -$config:ε, 1 + $config:ε)] let $ts := $tx[some $t in $ty satisfies abs(. - $t) < $config:ε] for $t in $ts return util:clamp($t, 0.0, 1.0) ) ) ) }
Function: cubic-t
declare function cubic-t($cubic as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double*
declare function cubic-t($cubic as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double*
Params
- cubic as map(xs:string,item()*)
- point as map(xs:string,item()*)
Returns
- xs:double*
declare function this:cubic-t($cubic as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double* { this:cubic-t($cubic, $point, $config:tolerance) }
Function: on-arc
declare function on-arc($arc as map(xs:string,item()*),
$point as map(xs:string,item()*),
$tolerance as xs:double) as xs:boolean
declare function on-arc($arc as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
on-arc()
Is the point on the arc (within ε)?
Note: will normalize the arc
Params
- arc as map(xs:string,item()*)
- point as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:on-arc( $arc as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double ) as xs:boolean { let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] return ( abs(point:distance($center, $point) - $radius) < $tolerance and ( util:twixt(this:angle($center, $point), $from-angle, $to-angle) or util:twixt(this:angle($center, $point)+360, $from-angle, $to-angle) ) ) }
Function: on-arc
declare function on-arc($arc as map(xs:string,item()*),
$point as map(xs:string,item()*)) as xs:boolean
declare function on-arc($arc as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean
Params
- arc as map(xs:string,item()*)
- point as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:on-arc( $arc as map(xs:string,item()*), $point as map(xs:string,item()*) ) as xs:boolean { this:on-arc($arc, $point, $config:tolerance) }
Function: normalize-arc
declare function normalize-arc($arc as map(xs:string,item()*)) as map(xs:string,item()*)
declare function normalize-arc($arc as map(xs:string,item()*)) as map(xs:string,item()*)
normalize-arc()
A lot of arc operations need us to know the actual arc center and
starting/ending angles. For large and flipped arcs that's get
problematic. Rather than repeat that code everywhere, encapsulate
it here. The normalized arc will always run in sweep order and may or
may not be large.
Params
- arc as map(xs:string,item()*): source arc
Returns
- map(xs:string,item()*)
declare function this:normalize-arc( $arc as map(xs:string,item()*) ) as map(xs:string,item()*) { let $arc := this:as-angle-arc($arc) let $flipped := $arc=>this:arc-flipped() let $large := $arc=>this:arc-large() let $angles := ($arc=>this:arc-angles())!util:remap-degrees(.) let $delta := $angles[2] - $angles[1] let $swap := ($large and ($delta < -180 or ($delta > 0 and $delta < 180)) ) or (not($large) and (($delta < 0 and abs($delta) < 180) or ($delta > 180)) ) let $adjust-from := ($large and $delta > 0 and $delta < 180) or (not($large) and $delta > 180) let $adjust-to := ($large and $delta < 0 and abs($delta) <= 180) or (not($large) and $delta <= -180) let $adjusted-from := if ($adjust-from) then $angles[1] + 360 else $angles[1] let $adjusted-to := if ($adjust-to) then $angles[2] + 360 else $angles[2] let $from-angle := if ($swap) then $adjusted-to else $adjusted-from let $to-angle := if ($swap) then $adjusted-from else $adjusted-to let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $from := point:destination($center, $from-angle, $radius) let $to := point:destination($center, $to-angle, $radius) let $flipped-center := if ($swap) then ( if ($flipped) then $center else affine:reflect($center, $from, $to) ) else ( if ($flipped) then affine:reflect($center, $from, $to) else $center ) return ( util:assert($from-angle <= $to-angle, "Normalized angles wonky"), util:assert((abs($to-angle - $from-angle) > 180) = $large, "Large flag wonky"), this:arc-by-angle( $flipped-center, $radius, $from-angle, $to-angle, false(), $large ) ) }
Function: arc-t
declare function arc-t($arc as map(xs:string,item()*),
$point as map(xs:string,item()*)) as xs:double*
declare function arc-t($arc as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double*
arc-t()
Compute the t value of the point wrt the arc edge.
Will normalize the arc
Params
- arc as map(xs:string,item()*)
- point as map(xs:string,item()*)
Returns
- xs:double*
declare function this:arc-t( $arc as map(xs:string,item()*), $point as map(xs:string,item()*) ) as xs:double* { if (point:same($point, this:start($arc))) then 0.0 else if (point:same($point, this:end($arc))) then 1.0 else ( let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $point-angle := this:angle($center, $point) return ( if (abs(point:distance($center, $point) - $radius) < $config:ε and ( util:twixt($point-angle, $from-angle, $to-angle) or util:twixt($point-angle + 360, $from-angle, $to-angle) ) ) then ( ($point-angle - $from-angle) div ($to-angle - $from-angle) ) else () ) ) }
Function: edge-t
declare function edge-t($edge as map(xs:string,item()*),
$point as map(xs:string,item()*)) as xs:double*
declare function edge-t($edge as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double*
Params
- edge as map(xs:string,item()*)
- point as map(xs:string,item()*)
Returns
- xs:double*
declare function this:edge-t( $edge as map(xs:string,item()*), $point as map(xs:string,item()*) ) as xs:double* { switch (this:kind($edge)) case "quad" return this:quad-t($edge, $point) case "cubic" return this:cubic-t($edge, $point) case "edge" return this:linear-t($edge, $point) case "arc" return this:arc-t($edge, $point) default return errors:error("GEOM-NOTIMPLEMENTED", ("edge-t", this:kind($edge))) }
Function: bounding-box
declare function bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*)
declare function bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*)
bounding-box()
Minimum box surrounding the region. Some approximation for non-linear
edges.
Params
- regions as map(xs:string,item()*)*
Returns
- map(xs:string,item()*)
declare function this:bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*) { let $boxes := ( for $region in $regions return switch (this:kind($region)) case "edge" return let $pts := this:points($region) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) case "quad" return ( (: P0 = start, P1 = control, P2 = end :) (: B(t) = (1-t)²·P0 + 2(1-t)t·P1 + t²·P2 :) (: B'(t) = a(1-t) + b where a = 2(B-A), b=2(C-B) :) (: A=P0 B=P1 C=P2 :) (: B'(t) = 0 where t = -a/(b-a) :) let $P0 := this:start($region) let $P1 := this:controls($region)[1] let $P2 := this:end($region) let $T := point:map3( function ($p0 as xs:double, $p1 as xs:double, $p2 as xs:double) as xs:double { let $A := $p0 let $B := $p1 let $C := $p2 let $a := 2*($B - $A) let $b := 2*($C - $B) return if ($a = $b) then xs:double("INF") else -$a div ($b - $a) }, $P0, $P1, $P2 ) let $pts := ( $P0, $P2, if (util:twixt(point:px($T), 0, 1)) then $region=>this:quad-point(point:px($T)) else (), if (util:twixt(point:py($T), 0, 1)) then $region=>this:quad-point(point:py($T)) else () ) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) ) case "cubic" return ( (: Cubic P1 to P4 with controls P2, P3 :) (: B(t) = (1-t)³·P1 + 3(1-t)²t·P2 + 3(1-t)t²·P3 + t³·P4 :) (: B'(t) = (1-t)²·A + 2(1-t)t·B + t²·C :) (: A=3(P2-P1), B=3(P3-P2), C=3(P4-P3) :) (: B'(t) = at² + bt + c = 0 => t = (-b ± √(b² - 4ac))/2a :) (: a = A - 2B + C = 3(-P1 + 3P2 - 3P3 + P4) :) (: b = 2(B - A) = 6(P1 - 2P2 + P3) :) (: c = A = 3(P2 - P1) :) let $P1 := this:start($region) let $P2 := this:controls($region)[1] let $P3 := this:controls($region)[2] let $P4 := this:end($region) let $ts := ( let $p1 := point:px($P1) let $p2 := point:px($P2) let $p3 := point:px($P3) let $p4 := point:px($P4) let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) let $c := 3*($p2 - $p1) return roots:quadratic-real-roots($a, $b, $c), let $p1 := point:py($P1) let $p2 := point:py($P2) let $p3 := point:py($P3) let $p4 := point:py($P4) let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) let $c := 3*($p2 - $p1) return roots:quadratic-real-roots($a, $b, $c) ) let $pts := ( $P1, $P4, for $t in $ts return ( if (util:twixt($t, 0, 1)) then $region=>this:cubic-point($t) else () ) ) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) ) case "arc" return ( let $arc := this:normalize-arc($region) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $from := $arc=>this:start() let $to := $arc=>this:end() (: If the arc is less than 180° we can do better than the whole : circle's bounding box by computing the intersection of the : tangent lines and finding the point from that to center at r : Tangent line to p has slope angle(center,p)+90 : Equation y = mx + b => p[y] = mp[x] + b => b = p[y] - mp[x] : Intersection of a1x + b1y + c1 = 0, a2x + b2y + c2 = 0 : is ((b1c2 - b2c1)/(a1b2 - a2b1), (c1a2 - c2a1)/(a1b2 - a2b1)) : a1=m b1=-1 c1=p[y]-mp[x] : a2=n b2=-1 c2=q[y]-mq[x] :) return ( if ($from-angle = $to-angle) then ( box:box($from, $from) ) else if ($to-angle - $from-angle < 180) then ( let $m := util:radians($from-angle - 90) let $a1 := $m let $b1 := -1 let $c1 := point:py($from) - $m*point:px($from) let $n := util:radians($to-angle - 90) let $a2 := $n let $b2 := -1 let $c2 := point:py($to) - $n*point:px($to) let $intersection := ( util:assert($a1*$b2 - $a2*$b1 != 0, "Non intersecting lines"), point:point( ($b1*$c2 - $b2*$c1) div ($a1*$b2 - $a2*$b1), ($c1*$a2 - $c2*$a1) div ($a1*$b2 - $a2*$b1) ) ) let $pts := ( $from, $to, point:destination($center, this:angle($center, $intersection), $radius) ) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) ) else ( ellipse:bounding-box(this:arc-circle($arc)) ) ) ) default return errors:error("GEOM-BADREGION", ($region, "bounding-box")) ) return ( if (empty($boxes)) then box:box(0,0,0,0) else if (empty(tail($boxes))) then $boxes else ( let $pts := $boxes!this:points(.) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) ) ) }
Function: line-intersection
declare function line-intersection($this as map(xs:string,item()*),
$that as map(xs:string,item()*)) as map(xs:string,item()*)?
declare function line-intersection($this as map(xs:string,item()*), $that as map(xs:string,item()*)) as map(xs:string,item()*)?
line-intersection()
Point of intersection, if any between lines defined as extension of
two edges.
Assumes linear edges: so wrong for other kinds of edges
Params
- this as map(xs:string,item()*)
- that as map(xs:string,item()*)
Returns
- map(xs:string,item()*)?
declare function this:line-intersection( $this as map(xs:string,item()*), $that as map(xs:string,item()*) ) as map(xs:string,item()*)? { (: edge: ((x1,y1), (x2,y2)) => line Ax + By = C : A = y2 - y1 : B = x1 - x2 : C = Ax1 + By1 : det = A1 * B2 - A2 * B1 : => ( (B2 * C1 - B1 * C2) / det, (A1 * C2 - A2 * C1) / det :) (: A = y2 - y1 ; Ax = A(this), Ay = A(that) :) let $A := ( point:py(this:end($this)) - point:py(this:start($this)), point:py(this:end($that)) - point:py(this:start($that)) ) (: B = x1 - x2 ; Bx = B(this), By = B(that) :) let $B := ( point:px(this:start($this)) - point:px(this:end($this)), point:px(this:start($that)) - point:px(this:end($that)) ) (: C = Ax1 + By1 ; Cx = C(this), Cy = C(that) :) let $C := ( v:px($A) * point:px(this:start($this)) + v:px($B) * point:py(this:start($this)), v:py($A) * point:px(this:start($that)) + v:py($B) * point:py(this:start($that)) ) let $det := v:determinant($A, $B) return ( if ($det = (0,-0)) then () else ( point:point( -v:determinant($B, $C) div $det, v:determinant($A, $C) div $det ) ) ) }
Function: intersection
declare function intersection($this as map(xs:string,item()*),
$that as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function intersection($this as map(xs:string,item()*), $that as map(xs:string,item()*)) as map(xs:string,item()*)*
intersection()
Point of intersection, if any between two edges
Params
- this as map(xs:string,item()*)
- that as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:intersection( $this as map(xs:string,item()*), $that as map(xs:string,item()*) ) as map(xs:string,item()*)* { switch (this:kind($this)) case "edge" return this:linear-intersection($this, $that) case "quad" return this:quad-intersection($this, $that) case "cubic" return this:cubic-intersection($this, $that) case "arc" return this:arc-intersection($this, $that) default return errors:error("GEOM-BADREGION", ($this, "intersection")) }
Function: linear-intersection
declare function linear-intersection($edge as map(xs:string,item()*),
$other as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function linear-intersection($edge as map(xs:string,item()*), $other as map(xs:string,item()*)) as map(xs:string,item()*)*
linear-intersection()
Point of intersection, if any between linear edge and some other edge
Params
- edge as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:linear-intersection( $edge as map(xs:string,item()*), $other as map(xs:string,item()*) ) as map(xs:string,item()*)* { if (this:kind($edge) != "edge") then errors:error("GEOM-BADREGION", ($edge, "linear-intersection")) else ( switch(this:kind($other)) case "edge" return ( let $p := this:line-intersection($edge, $other) return ( if (empty($p)) then () else if (this:on-segment-colinear($edge, $p) and this:on-segment-colinear($other, $p)) then $p else () ) ) case "quad" return this:quad-intersection($other, $edge) case "cubic" return this:cubic-intersection($other, $edge) case "arc" return this:arc-intersection($other, $edge) default return errors:error("GEOM-BADREGION", ($edge, "linear-intersection")) ) }
Function: quad-edge-intersection-ts
declare function quad-edge-intersection-ts($quad as map(xs:string,item()*),
$other as map(xs:string,item()*),
$tolerance as xs:double) as xs:double*
declare function quad-edge-intersection-ts($quad as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:double*
Params
- quad as map(xs:string,item()*)
- other as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:double*
declare function this:quad-edge-intersection-ts( $quad as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double ) as xs:double* { if (this:kind($quad) != "quad") then errors:error("GEOM-BADREGION", ($quad, "quad-edge-intersection-ts")) else if (this:kind($other) != "edge") then errors:error("GEOM-BADREGION", ($quad, "quad-edge-intersection-ts")) else ( (: https://pomax.github.io/bezierinfo/ :) let $angle := this:angle($other) let $rotated-edge := this:mutate($other, function ($p as map(xs:string,item()*)) as map(xs:string,item()*) {affine:rotate($p, $angle, this:start($other))} ) let $translation := point:py(this:start($rotated-edge)) let $translated-edge := this:mutate($rotated-edge, function ($p as map(xs:string,item()*)) as map(xs:string,item()*) {affine:translate($p, 0, -$translation)} ) let $translated-quad := this:mutate($quad, function ($p as map(xs:string,item()*)) as map(xs:string,item()*) {affine:rotate($p, $angle, this:start($other))=>affine:translate(0,-$translation)} ) let $P0 := this:start($translated-quad) let $P1 := this:controls($translated-quad)[1] let $P2 := this:end($translated-quad) let $ts := ( let $p0 := point:px($P0) let $p1 := point:px($P1) let $p2 := point:px($P2) let $a := $p0 - 2*$p1 + $p2 let $b := 2*$p1 - 2*$p0 let $c := $p0 return roots:quadratic-real-roots($a, $b, $c) , let $p0 := point:py($P0) let $p1 := point:py($P1) let $p2 := point:py($P2) let $a := $p0 - 2*$p1 + $p2 let $b := 2*$p1 - 2*$p0 let $c := $p0 return roots:quadratic-real-roots($a, $b, $c) ) return ( distinct-values( for $t in $ts where ( util:twixt($t, 0, 1) and ( let $qp := this:quad-point($translated-quad, $t) return abs(point:py($qp)) < $tolerance and util:twixt(point:px($qp), point:px(this:start($translated-edge)), point:px(this:end($translated-edge))) ) ) return $t ) ) ) }
Function: quad-edge-intersection-ts
declare function quad-edge-intersection-ts($quad as map(xs:string,item()*),
$other as map(xs:string,item()*)) as xs:double*
declare function quad-edge-intersection-ts($quad as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:double*
Params
- quad as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- xs:double*
declare function this:quad-edge-intersection-ts( $quad as map(xs:string,item()*), $other as map(xs:string,item()*) ) as xs:double* { this:quad-edge-intersection-ts($quad, $other, $config:tolerance) }
Function: quad-intersection
declare function quad-intersection($quad as map(xs:string,item()*),
$other as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function quad-intersection($quad as map(xs:string,item()*), $other as map(xs:string,item()*)) as map(xs:string,item()*)*
quad-intersection()
Point of intersection, if any between quad edge and some other edge
Params
- quad as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:quad-intersection( $quad as map(xs:string,item()*), $other as map(xs:string,item()*) ) as map(xs:string,item()*)* { if (this:kind($quad) != "quad") then errors:error("GEOM-BADREGION", ($quad, "quad-intersection")) else ( switch(this:kind($other)) case "edge" return ( let $good-ts := this:quad-edge-intersection-ts($quad, $other) for $t in $good-ts return this:quad-point($quad, $t) ) case "quad" return ( if (box:intersects-box(this:bounding-box($quad), this:bounding-box($other))) then ( if (this:quad-length($quad) < $config:ε and this:quad-length($other) < $config:ε) then ( this:midpoint($quad) ) else ( let $pts := let $quadsubs := this:slice-quad($quad, 0.5) let $othersubs := this:slice-quad($other, 0.5) for $subquad in $quadsubs for $subother in $othersubs return this:intersection($subquad, $subother) let $dpts := for $p in $pts return point:decimal($p, $this:precision) for $i in 1 to count($dpts) where every $j in 1 to $i - 1 satisfies ( not(point:same($dpts[$i], $dpts[$j])) ) return $pts[$i] ) ) else ( ) ) case "cubic" return this:cubic-intersection($other, $quad) default return errors:error("GEOM-BADREGION", ($other, "quad-intersection")) ) }
Function: cubic-intersection
declare function cubic-intersection($cubic as map(xs:string,item()*),
$other as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function cubic-intersection($cubic as map(xs:string,item()*), $other as map(xs:string,item()*)) as map(xs:string,item()*)*
cubic-intersection()
Point of intersection, if any between cubic edge and some other edge
Params
- cubic as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:cubic-intersection( $cubic as map(xs:string,item()*), $other as map(xs:string,item()*) ) as map(xs:string,item()*)* { if (this:kind($cubic) != "cubic") then errors:error("GEOM-BADREGION", ($cubic, "cubic-intersection")) else ( switch(this:kind($other)) case "edge" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other))) then ( if (this:linear-length($other) < $config:ε and this:cubic-length($cubic, 4) < $config:ε) then this:midpoint($cubic) else ( let $pts := let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-linear($other, 0.5) for $subcubic in $cubicsubs for $subother in $othersubs return this:intersection($subcubic, $subother) let $dpts := for $p in $pts return point:decimal($p, $this:precision) for $i in 1 to count($dpts) where every $j in 1 to $i - 1 satisfies ( not(point:same($dpts[$i], $dpts[$j])) ) return $pts[$i] ) ) else ( ) ) case "quad" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other))) then ( if (this:quad-length($other) < $config:ε and this:cubic-length($cubic, 4) < $config:ε) then this:midpoint($cubic) else ( let $pts := let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-quad($other, 0.5) for $subcubic in $cubicsubs for $subother in $othersubs return this:intersection($subcubic, $subother) let $dpts := for $p in $pts return point:decimal($p, $this:precision) for $i in 1 to count($dpts) where every $j in 1 to $i - 1 satisfies ( not(point:same($dpts[$i], $dpts[$j])) ) return $pts[$i] ) ) else ( ) ) case "cubic" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other))) then ( if (this:cubic-length($cubic, 4) < $config:ε and this:cubic-length($other, 4) < $config:ε) then this:midpoint($cubic) else ( let $pts := let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-cubic($other, 0.5) for $subcubic in $cubicsubs for $subother in $othersubs return this:intersection($subcubic, $subother) let $dpts := for $p in $pts return point:decimal($p, $this:precision) for $i in 1 to count($dpts) where every $j in 1 to $i - 1 satisfies ( not(point:same($dpts[$i], $dpts[$j])) ) return $pts[$i] ) ) else ( ) ) default return errors:error("GEOM-BADREGION", ($other, "cubic-intersection")) ) }
Function: arc-intersection
declare function arc-intersection($arc as map(xs:string,item()*),
$other as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function arc-intersection($arc as map(xs:string,item()*), $other as map(xs:string,item()*)) as map(xs:string,item()*)*
Params
- arc as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:arc-intersection( $arc as map(xs:string,item()*), $other as map(xs:string,item()*) ) as map(xs:string,item()*)* { if (this:kind($arc) != "arc") then errors:error("GEOM-BADREGION", ($arc, "arc-intersection")) else ( switch(this:kind($other)) case "edge" return ( let $arc := this:normalize-arc($arc) (: https://math.stackexchange.com/questions/228841/how-do-i-calculate-the-intersections-of-a-straight-line-and-a-circle :) (: line y=mx + c; m = (y2-y1)/(x2-x1), c=y1 :) let $r := this:arc-radius($arc) let $p := point:px(this:arc-center($arc)) let $q := point:py(this:arc-center($arc)) let $x1 := point:px(this:start($other)) let $y1 := point:py(this:start($other)) let $x2 := point:px(this:end($other)) let $y2 := point:py(this:end($other)) let $pts := ( if (abs($x1 - $x2) < $config:ε) then ( (: vertical line :) let $k := $x1 let $A := 1 let $B := -2*$q let $C := $p*$p + $q*$q - $r*$r - 2*$k*$p + $k*$k where $B*$B >= 4*$A*$C return ( for $y in roots:quadratic-real-roots($A, $B, $C) return point:point($k, $y) ) ) else ( let $m := ($y2 - $y1) div ($x2 - $x1) let $c := $y1 - $m*$x1 let $A := $m*$m + 1 let $B := 2*($m*$c - $m*$q - $p) let $C := $q*$q - $r*$r + $p*$p - 2*$c*$q + $c*$c where $B*$B >= 4*$A*$C return ( for $x in roots:quadratic-real-roots($A, $B, $C) return point:point($x, $m*$x + $c) ) ) ) return ( for $pt in $pts where this:on-segment-colinear($other, $pt) and this:on-arc($arc, $pt) return $pt ) ) case "quad" return ( errors:error("GEOM-NOTIMPLEMENTED", ("arc-intersection", this:kind($other))) ) case "cubic" return ( errors:error("GEOM-NOTIMPLEMENTED", ("arc-intersection", this:kind($other))) ) case "arc" return ( errors:error("GEOM-NOTIMPLEMENTED", ("arc-intersection", this:kind($other))) ) default return errors:error("GEOM-BADREGION", ($other, "arc-intersection")) ) }
Function: edge-intersects-edge
declare function edge-intersects-edge($edge as map(xs:string,item()*),
$other as map(xs:string,item()*)) as xs:boolean
declare function edge-intersects-edge($edge as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
linear edge to linear edge intersection
Params
- edge as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:edge-intersects-edge( $edge as map(xs:string,item()*), $other as map(xs:string,item()*) ) as xs:boolean { this:edge-intersects-edge($edge, $other, $config:tolerance) }
Function: edge-intersects-edge
declare function edge-intersects-edge($edge as map(xs:string,item()*),
$other as map(xs:string,item()*),
$tolerance as xs:double) as xs:boolean
declare function edge-intersects-edge($edge as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
Params
- edge as map(xs:string,item()*)
- other as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:edge-intersects-edge( $edge as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double ) as xs:boolean { (: See https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/ :) let $o1 := point:sorientation(this:start($edge), this:end($edge), this:start($other)) let $o2 := point:sorientation(this:start($edge), this:end($edge), this:end($other)) let $o3 := point:sorientation(this:start($other), this:end($other), this:start($edge)) let $o4 := point:sorientation(this:start($other), this:end($other), this:end($edge)) return ( ($o1 != $o2 and $o3 != $o4) or (: $other.start colinear with and on segment $edge :) ($o1=0 and this:on-segment-colinear($edge, this:start($other), $tolerance)) or (: $other.end colinear with and on segment $edge :) ($o2=0 and this:on-segment-colinear($edge, this:end($other), $tolerance)) or (: $edge.start colinear with and on segment $other :) ($o3=0 and this:on-segment-colinear($other, this:start($edge), $tolerance)) or (: $edge.end colinear with and on segment $other :) ($o4=0 and this:on-segment-colinear($other, this:end($edge), $tolerance)) ) }
Function: linear-intersects
declare function linear-intersects($edge as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
declare function linear-intersects($edge as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
linear-intersects()
Edge (straight) intersects some other region kind
Params
- edge as map(xs:string,item()*)
- other as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:linear-intersects($edge as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { switch (this:kind($other)) case "point" return this:on-segment($edge, $other, $tolerance) case "edge" return this:edge-intersects-edge($edge, $other, $tolerance) case "quad" return this:quad-intersects($other, $edge, $tolerance) case "cubic" return this:cubic-intersects($other, $edge, $tolerance) case "arc" return this:arc-intersects($other, $edge, $tolerance) default return errors:error("GEOM-BADREGION", ($other, "linear-intersects")) }
Function: linear-intersects
declare function linear-intersects($edge as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
declare function linear-intersects($edge as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
Params
- edge as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:linear-intersects($edge as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:linear-intersects($edge, $other, $config:tolerance) }
Function: quad-intersects
declare function quad-intersects($quad as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
declare function quad-intersects($quad as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
quad-intersects()
Quad edge intersects some other region kind
Params
- quad as map(xs:string,item()*)
- other as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:quad-intersects($quad as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { switch (this:kind($other)) case "point" return this:on-quad($quad, $other, $tolerance) case "edge" return exists(this:quad-edge-intersection-ts($quad, $other, $tolerance)) case "quad" return ( if (box:intersects-box(this:bounding-box($quad), this:bounding-box($other), $tolerance)) then ( if (this:quad-length($quad) < $config:ε and this:quad-length($other) < $config:ε) then true() else ( let $quadsubs := this:slice-quad($quad, 0.5) let $othersubs := this:slice-quad($other, 0.5) return ( some $subquad in $quadsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subquad, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "cubic" return this:cubic-intersects($other, $quad, $tolerance) case "arc" return this:arc-intersects($other, $quad, $tolerance) default return errors:error("GEOM-BADREGION", ($other, "quad-intersects")) }
Function: quad-intersects
declare function quad-intersects($quad as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
declare function quad-intersects($quad as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
Params
- quad as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:quad-intersects($quad as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:quad-intersects($quad, $other, $config:tolerance) }
Function: cubic-intersects
declare function cubic-intersects($cubic as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
declare function cubic-intersects($cubic as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
cubic-intersects()
Cubic edge intersects some other region kind
Params
- cubic as map(xs:string,item()*)
- other as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:cubic-intersects($cubic as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { switch (this:kind($other)) case "point" return ( this:on-cubic($cubic, $other, $tolerance) ) case "edge" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other), $tolerance)) then ( if (this:linear-length($other) < $tolerance and this:cubic-length($cubic, 4) < $tolerance) then true() else ( let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-linear($other, 0.5) return ( some $subcubic in $cubicsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subcubic, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "quad" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other), $tolerance)) then ( if (this:quad-length($other) < $tolerance and this:cubic-length($cubic, 4) < $tolerance) then true() else ( let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-quad($other, 0.5) return ( some $subcubic in $cubicsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subcubic, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "cubic" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other), $tolerance)) then ( if (this:cubic-length($cubic, 4) < $tolerance and this:cubic-length($other, 4) < $tolerance) then true() else ( let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-cubic($other, 0.5) return ( some $subcubic in $cubicsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subcubic, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "arc" return this:arc-intersects($other, $cubic, $tolerance) default return errors:error("GEOM-BADREGION", ($other, "cubic-intersects")) }
Function: cubic-intersects
declare function cubic-intersects($cubic as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
declare function cubic-intersects($cubic as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
Params
- cubic as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:cubic-intersects($cubic as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:cubic-intersects($cubic, $other, $config:tolerance) }
Function: arc-intersects
declare function arc-intersects($arc as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
declare function arc-intersects($arc as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
arc-intersects()
Arc edge intersects some other region kind
Params
- arc as map(xs:string,item()*)
- other as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:arc-intersects($arc as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { if (point:same(this:start($arc), this:end($arc), $tolerance)) then ( if (this:arc-large($arc)) then ( this:edge-intersects(this:arc-circle(this:normalize-arc($arc)), $other, $tolerance) ) else ( this:edge-intersects(this:start($arc), $other, $tolerance) ) ) else switch (this:kind($other)) case "point" return this:on-arc($arc, $other, $tolerance) case "edge" return let $arc := this:normalize-arc($arc) (: https://math.stackexchange.com/questions/228841/how-do-i-calculate-the-intersections-of-a-straight-line-and-a-circle :) (: line y=mx + x; m = (y2-y1)/(x2-x1), c=y1 :) let $r := this:arc-radius($arc) let $p := point:px(this:arc-center($arc)) let $q := point:py(this:arc-center($arc)) let $x1 := point:px(this:start($other)) let $y1 := point:py(this:start($other)) let $x2 := point:px(this:end($other)) let $y2 := point:py(this:end($other)) let $pts := if (abs($x1 - $x2) < $config:ε) then ( (: vertical line :) let $k := $x1 let $A := 1 let $B := -2*$q let $C := $p*$p + $q*$q - $r*$r - 2*$k*$p + $k*$k where $B*$B >= 4*$A*$C return ( for $y in roots:quadratic-real-roots($A, $B, $C) return point:point($k, $y) ) ) else ( let $m := ($y2 - $y1) div ($x2 - $x1) let $c := $y1 - $m*$x1 let $A := $m*$m + 1 let $B := 2*($m*$c - $m*$q - $p) let $C := $q*$q - $r*$r + $p*$p - 2*$c*$q + $c*$c where $B*$B >= 4*$A*$C return ( for $x in roots:quadratic-real-roots($A, $B, $C) return point:point($x, $m*$x + $c) ) ) return ( some $pt in $pts satisfies this:on-segment-colinear($other, $pt, $tolerance) and this:on-arc($arc, $pt, $tolerance) ) case "quad" return ( if (box:intersects-box(this:bounding-box($arc), this:bounding-box($other), $tolerance)) then ( if (this:arc-length($arc) < $tolerance and this:quad-length($other) < $tolerance) then ( true() ) else ( let $arcsubs := this:slice-arc($arc, 0.5) let $othersubs := this:slice-quad($other, 0.5) return ( some $subarc in $arcsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subarc, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "cubic" return ( if (box:intersects-box(this:bounding-box($arc), this:bounding-box($other), $tolerance)) then ( if (this:arc-length($arc) < $tolerance and this:cubic-length($other, 4) < $tolerance) then true() else ( let $arcsubs := this:slice-arc($arc, 0.5) let $othersubs := this:slice-cubic($other, 0.5) return ( some $subarc in $arcsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subarc, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "arc" return let $arc := this:normalize-arc($arc) return ( ellipse:ellipse-intersects-ellipse(this:arc-circle($arc), this:arc-circle($other), $tolerance) and ( (: https://stackoverflow.com/questions/3349125/circle-circle-intersection-points :) let $d := point:distance(this:arc-center($arc), this:arc-center($other)) return if ($d <= $tolerance) then ( some $pt in (this:start($other), this:end($other)) satisfies this:on-arc($arc, $pt, $tolerance) ) else ( let $r1 := this:arc-radius($arc) let $p1 := this:arc-center($arc) let $x1 := point:px($p1) let $y1 := point:py($p1) let $r2 := this:arc-radius($other) let $p2 := this:arc-center($other) let $x2 := point:px($p2) let $y2 := point:py($p2) let $l := ($r1*$r1 - $r2*$r2 + $d*$d) div (2*$d) let $h := math:sqrt($r1*$r1 - $l*$l) let $dp := $p2=>point:sub($p1) let $dx := $x2 - $x1 let $dy := $y2 - $y1 let $mid := $p1=>point:add($dp=>point:times($l div $d)) let $pts := ( $mid=>point:add($dp=>point:times($h div $d)), $mid=>point:sub($dp=>point:times($h div $d)) ) return ( some $pt in $pts satisfies this:on-arc($arc, $pt, $tolerance) and this:on-arc($other, $pt, $tolerance) ) ) ) ) default return errors:error("GEOM-BADREGION", ($other, "arc-intersects")) }
Function: arc-intersects
declare function arc-intersects($arc as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
declare function arc-intersects($arc as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
Params
- arc as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:arc-intersects($arc as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:arc-intersects($arc, $other, $config:tolerance) }
Function: edge-intersects
declare function edge-intersects($region as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
declare function edge-intersects($region as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean
Params
- region as map(xs:string,item()*)
- other as map(xs:string,item()*)
- tolerance as xs:double
Returns
- xs:boolean
declare function this:edge-intersects($region as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { switch (this:kind($region)) case "point" return this:edge-intersects($other, $region, $tolerance) case "edge" return this:linear-intersects($region, $other, $tolerance) case "quad" return this:quad-intersects($region, $other, $tolerance) case "cubic" return this:cubic-intersects($region, $other, $tolerance) case "arc" return this:arc-intersects($region, $other, $tolerance) default return errors:error("GEOM-BADREGION", ($other, "edge-intersects")) }
Function: edge-intersects
declare function edge-intersects($region as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
declare function edge-intersects($region as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean
Params
- region as map(xs:string,item()*)
- other as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:edge-intersects($region as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:edge-intersects($region, $other, $config:tolerance) }
Function: tangent
declare function tangent($region as map(xs:string,item()*),
$t as xs:double) as map(xs:string,item()*)
declare function tangent($region as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
tangent()
Return the tangent vector to the given point on the edge (2D)
Params
- region as map(xs:string,item()*)
- t as xs:double
Returns
- map(xs:string,item()*)
declare function this:tangent( $region as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*) { switch(this:kind($region)) case "edge" return ( this:end($region)=>point:sub(this:start($region))=>point:normalize() ) case "quad" return ( (: : Derivative: : P0 = start, P1 = control, P2 = end : B(t) = (1-t)²·P0 + 2(1-t)t·P1 + t²·P2 : B'(t) = (1-t)·A + t·B : B'(t) = a(1-t) + b where a = 2(B-A), b=2(C-A) : A=P0 B=P1 :) let $P0 := this:start($region) let $P1 := this:controls($region)[1] let $P2 := this:end($region) return ( point:map3( function ($p0 as xs:double, $p1 as xs:double, $p2 as xs:double) as xs:double { let $A := $p0 let $B := $p1 let $C := $p2 let $a := 2*($B - $A) let $b := 2*($C - $B) return $a*(1 - $t) + $b }, $P0, $P1, $P2 )=>point:normalize() ) ) case "cubic" return ( (: : Derivative : Cubic P1 to P4 with controls P2, P3 : B(t) = (1-t)³·P1 + 3(1-t)²t·P2 + 3(1-t)t²·P3 + t³·P2 : B'(t) = (1-t)²·A + 2(1-t)t·B + t²·C : A=3(P2-P1), B=3(P3-P2), C=3(P4-P3) : B'(t) = at² + bt + c = 0 : a = A - 2B + C = 3(-P1 + 3P2 - 3P3 + P4) : b = 2(B - A) = 6(P1 - 2P2 + P3) : c = A = 3(P2 - P1) :) let $P1 := this:start($region) let $P2 := this:controls($region)[1] let $P3 := this:controls($region)[2] let $P4 := this:end($region) return ( point:map4( function ($p1 as xs:double, $p2 as xs:double, $p3 as xs:double, $p4 as xs:double) as xs:double { let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) let $c := 3*($p2 - $p1) return $a*$t*$t + $b*$t + $c }, $P1, $P2, $P3, $P4 ) )=>point:normalize() ) case "arc" return ( let $region := this:normalize-arc($region) let $c := this:arc-center($region) let $r := this:arc-radius($region) let $ellipse := ellipse:circle($c, $r) let $arc-angles := this:arc-angles($region) let $start-t := $arc-angles[1] div 360 let $end-t := $arc-angles[2] div 360 (: t for ellipse: ts + (te - ts)*ta :) let $revised-t := $start-t + ($end-t - $start-t)*$t let $p := $ellipse=>ellipse:ellipse-point($revised-t) let $angle := point:angle($c, $p) return ( this:edge( point:destination($p, $angle - 90, 1), point:destination($p, $angle + 90, 1) )=>point:normalize() ) ) case "ellipse" return ( (: May seem odd for this to be here, but edge depends on ellipse : and this is making an edge :) let $c := ellipse:center($region) let $p := $region=>ellipse:ellipse-point($t) let $angle := point:angle($c, $p) return ( this:edge( point:destination($p, $angle - 90, 1), point:destination($p, $angle + 90, 1) )=>point:normalize() ) ) default return errors:error("GEOM-NOTIMPLEMENTED", ("tangent", this:kind($region))) }
Function: curvature
declare function curvature($edge as map(xs:string,item()*),
$t as xs:double) as xs:double
declare function curvature($edge as map(xs:string,item()*), $t as xs:double) as xs:double
curvature()
Return the curvature at the given point on the edge (2D)
κ(t) = |det(B(t),B''(t)|/||B'(t)||³
Params
- edge as map(xs:string,item()*)
- t as xs:double
Returns
- xs:double
declare function this:curvature( $edge as map(xs:string,item()*), $t as xs:double ) as xs:double { abs(this:signed-curvature($edge, $t)) }
Function: signed-curvature
declare function signed-curvature($edge as map(xs:string,item()*),
$t as xs:double) as xs:double
declare function signed-curvature($edge as map(xs:string,item()*), $t as xs:double) as xs:double
signed-curvature()
Return the signed curvature at the given point on the edge (2D)
k(t) = det(B(t),B''(t)/||B'(t)||³
Params
- edge as map(xs:string,item()*)
- t as xs:double
Returns
- xs:double
declare function this:signed-curvature( $edge as map(xs:string,item()*), $t as xs:double ) as xs:double { switch(this:kind($edge)) case "edge" return ( 0 ) case "quad" return ( (: : Derivative: : P0 = start, P1 = control, P2 = end : B(t) = (1-t)²·P0 + 2(1-t)t·P1 + t²·P2 : B(t) = (P0 - 2P1 + P2)t² + (-2P0 + 2P1)t + P0 : B'(t) = a(1-t) + b where a = 2(B-A), b=2(C-A) : B''(t) = -a where a=2(B-A) i.e. 2(A-B) : A=P0 B=P1 C=P2 :) let $P0 := this:start($edge)=>point:pcoordinates() let $P1 := (this:controls($edge)[1])=>point:pcoordinates() let $P2 := this:end($edge)=>point:pcoordinates() let $Bt := ( v:map3( function ($p0 as xs:double, $p1 as xs:double, $p2 as xs:double) as xs:double { ($p0 - 2*$p1 + $p2)*$t*$t + 2*($p1 - $p0) + $p0 }, $P0, $P1, $P2 ) ) let $Btder := ( v:map3( function ($p0 as xs:double, $p1 as xs:double, $p2 as xs:double) as xs:double { let $A := $p0 let $B := $p1 let $C := $p2 let $a := 2*($B - $A) let $b := 2*($C - $B) return $a*(1 - $t) + $b }, $P0, $P1, $P2 ) ) let $Btdoubleder := v:map2( function ($p0 as xs:double, $p1 as xs:double) as xs:double { 2*($p0 - $p1) }, $P0, $P1 ) return ( (: k(t) = det(B(t),B''(t)/||B'(t)||³ :) v:determinant($Bt, $Btdoubleder) div math:pow(v:magnitude($Btder), 3) ) ) case "cubic" return ( (: : Derivative : Cubic P1 to P4 with controls P2, P3 : B(t) = (1-t)³·P1 + 3(1-t)²t·P2 + 3(1-t)t²·P3 + t³·P2 : B'(t) = (1-t)²·A + 2(1-t)t·B + t²·C : A=3(P2-P1), B=3(P3-P2), C=3(P4-P3) : B'(t) = at² + bt + c = 0 : a = A - 2B + C = 3(-P1 + 3P2 - 3P3 + P4) : b = 2(B - A) = 6(P1 - 2P2 + P3) : c = A = 3(P2 - P1) : B''(t) = 2at + b :) let $P1 := this:start($edge)=>point:pcoordinates() let $P2 := (this:controls($edge)[1])=>point:pcoordinates() let $P3 := (this:controls($edge)[2])=>point:pcoordinates() let $P4 := this:end($edge)=>point:pcoordinates() let $Bt := ( v:map4( function ($p1 as xs:double, $p2 as xs:double, $p3 as xs:double, $p4 as xs:double) as xs:double { let $one-t := 1 - $t return ( math:pow($one-t, 3)*$p1 + 2*math:pow($one-t, 2)*$t*$p2 + 2*$one-t*math:pow($t, 2)*$p3 + math:pow($t, 3)*$p2 ) }, $P1, $P2, $P3, $P4 ) ) let $Btder := ( v:map4( function ($p1 as xs:double, $p2 as xs:double, $p3 as xs:double, $p4 as xs:double) as xs:double { let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) let $c := 3*($p2 - $p1) return $a*$t*$t + $b*$t + $c }, $P1, $P2, $P3, $P4 ) ) let $Btdoubleder := ( v:map4( function ($p1 as xs:double, $p2 as xs:double, $p3 as xs:double, $p4 as xs:double) as xs:double { let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) return 2*$a*$t + $b }, $P1, $P2, $P3, $P4 ) ) return ( (: k(t) = det(B(t),B''(t)/||B'(t)||³ :) v:determinant($Bt, $Btdoubleder) div math:pow(v:magnitude($Btder), 3) ) ) case "arc" return if (this:arc-flipped($edge)) then -1 div this:arc-radius($edge) else 1 div this:arc-radius($edge) default return errors:error("GEOM-NOTIMPLEMENTED", ("curvature", this:kind($edge))) }
Function: osculating-circle
declare function osculating-circle($edge as map(xs:string,item()*),
$t as xs:double) as map(xs:string,item()*)
declare function osculating-circle($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)
osculating-circle()
Compute the osculating circle at the given point on the edge.
Returns a zero radius circle at the edge point for infinite or NaN
curvatures.
Params
- edge as map(xs:string,item()*)
- t as xs:double
Returns
- map(xs:string,item()*)
declare function this:osculating-circle( $edge as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*) { (: : : N(t) = normal unit vector : T'(t) = k(t)N(t) : k(t) is signed version of κ(t) : R(t) = 1/κ(t) : C(t) = B(t) + (1/κ²(t))T'(t) center of osculating circle : = B(t) + (1/κ²(t))k(t)N(t) : = B(t) + R²(t)(sign(k(t))/R(t))N(t) : = B(t) + sign(k(t))R(t)N(t) :) let $k := $edge=>this:signed-curvature($t) let $κ := abs($k) let $n := $edge=>this:tangent($t)=>point:perpendicular()=>point:normalize() let $p := $edge=>this:edge-point($t) return ( if (not($κ=$κ)) then ellipse:circle($p, 0) else if ($κ = xs:double("INF")) then ellipse:circle($p, 0) else ( let $radius := if ($κ = 0) then this:length($edge) div 2 else 1 div $κ let $sign := if ($κ = 0) then 1 else util:zsign($k) let $center := $n=>point:times($sign*$radius)=>point:add($p) return ( util:assert(not($radius=xs:double("INF")), "Bad radius: INF "||($κ=0)), util:assert($radius=$radius, "Bad radius: NaN"), ellipse:circle($center, $radius) ) ) ) }
Function: interpolate-edge-using
declare function interpolate-edge-using($edge as map(xs:string,item()*),
$divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
declare function interpolate-edge-using($edge as map(xs:string,item()*), $divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
Params
- edge as map(xs:string,item()*)
- divisions as function(item())asxs:double*
Returns
- map(xs:string,item()*)*
declare function this:interpolate-edge-using( $edge as map(xs:string,item()*), $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { let $from := this:start($edge) let $to := this:end($edge) let $d := point:max-dimension(($from, $to)) for $t in $divisions($edge) order by $t ascending return ( point:map2( function ($a as xs:double, $b as xs:double) as xs:double { $a + $t * ($b - $a) }, $from, $to, $d ) ) }
Function: interpolate-edge
declare function interpolate-edge($n as xs:integer,
$edge as map(xs:string,item()*),
$exclusive as xs:boolean) as map(xs:string,item()*)*
declare function interpolate-edge($n as xs:integer, $edge as map(xs:string,item()*), $exclusive as xs:boolean) as map(xs:string,item()*)*
interpolate-edge()
Interpolate a linear edge.
Params
- n as xs:integer: number of interpolating points
- edge as map(xs:string,item()*): the edge
- exclusive as xs:boolean: exclude end point? default=false
Returns
- map(xs:string,item()*)*
declare function this:interpolate-edge( $n as xs:integer, $edge as map(xs:string,item()*), $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-edge-using($edge, function ($region as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, $exclusive) } ) }
Function: interpolate-edge
declare function interpolate-edge($n as xs:integer,
$edge as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function interpolate-edge($n as xs:integer, $edge as map(xs:string,item()*)) as map(xs:string,item()*)*
Params
- n as xs:integer
- edge as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:interpolate-edge( $n as xs:integer, $edge as map(xs:string,item()*) ) as map(xs:string,item()*)* { this:interpolate-edge($n, $edge, false()) }
Function: interpolate-quadratic-using
declare function interpolate-quadratic-using($quadratic as map(xs:string,item()*),
$divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
declare function interpolate-quadratic-using($quadratic as map(xs:string,item()*), $divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
Params
- quadratic as map(xs:string,item()*)
- divisions as function(item())asxs:double*
Returns
- map(xs:string,item()*)*
declare function this:interpolate-quadratic-using( $quadratic as map(xs:string,item()*), $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { let $from := $quadratic=>this:start() let $to := $quadratic=>this:end() let $controls := $quadratic=>this:controls() let $d := point:max-dimension(($from, $to, $controls[1])) for $t in $divisions($quadratic) order by $t ascending return ( point:map3( function ($a as xs:double, $b as xs:double, $c as xs:double) as xs:double { (1 - $t)*(1 - $t)*$a + 2*(1 - $t)*$t*$c + $t*$t*$b }, $from, $to, $controls[1], $d ) ) }
Function: interpolate-quadratic
declare function interpolate-quadratic($n as xs:integer,
$quadratic as map(xs:string,item()*),
$exclusive as xs:boolean) as map(xs:string,item()*)*
declare function interpolate-quadratic($n as xs:integer, $quadratic as map(xs:string,item()*), $exclusive as xs:boolean) as map(xs:string,item()*)*
interpolate-quadratic()
Interpolate a quadratic edge.
B(t)=(1-t)²P0 + 2(1-t)tP1 + t²P2, 0 <= t <= 1
Params
- n as xs:integer: number of interpolating points
- quadratic as map(xs:string,item()*) the edge
- exclusive as xs:boolean: exclude end point? default=false
Returns
- map(xs:string,item()*)*
declare function this:interpolate-quadratic( $n as xs:integer, $quadratic as map(xs:string,item()*), $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-quadratic-using($quadratic, function ($region as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, $exclusive) } ) }
Function: interpolate-quadratic
declare function interpolate-quadratic($n as xs:integer,
$quadratic as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function interpolate-quadratic($n as xs:integer, $quadratic as map(xs:string,item()*)) as map(xs:string,item()*)*
Params
- n as xs:integer
- quadratic as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:interpolate-quadratic( $n as xs:integer, $quadratic as map(xs:string,item()*) ) as map(xs:string,item()*)* { this:interpolate-quadratic($n, $quadratic, false()) }
Function: interpolate-cubic-using
declare function interpolate-cubic-using($cubic as map(xs:string,item()*),
$divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
declare function interpolate-cubic-using($cubic as map(xs:string,item()*), $divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
Params
- cubic as map(xs:string,item()*)
- divisions as function(item())asxs:double*
Returns
- map(xs:string,item()*)*
declare function this:interpolate-cubic-using( $cubic as map(xs:string,item()*), $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { let $from := $cubic=>this:start() let $to := $cubic=>this:end() let $controls := $cubic=>this:controls() let $d := point:max-dimension(($from, $to, $controls[1], $controls[2])) for $t in $divisions($cubic) order by $t ascending return ( point:map4( function ($a as xs:double, $b as xs:double, $c as xs:double, $d as xs:double) as xs:double { (1 - $t)*(1 - $t)*(1 - $t)*$a + 3*(1 - $t)*(1 - $t)*$t*$c + 3*(1 - $t)*$t*$t*$d + $t*$t*$t*$b }, $from, $to, $controls[1], $controls[2], $d ) ) }
Function: interpolate-cubic
declare function interpolate-cubic($n as xs:integer,
$cubic as map(xs:string,item()*),
$exclusive as xs:boolean) as map(xs:string,item()*)*
declare function interpolate-cubic($n as xs:integer, $cubic as map(xs:string,item()*), $exclusive as xs:boolean) as map(xs:string,item()*)*
interpolate-cubic()
Interpolate a cubic edge.
B(t)=(1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3, 0 <= t <= 1
Params
- n as xs:integer: number of interpolating points
- cubic as map(xs:string,item()*) the edge
- exclusive as xs:boolean: exclude end point? default=false
Returns
- map(xs:string,item()*)*
declare function this:interpolate-cubic( $n as xs:integer, $cubic as map(xs:string,item()*), $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-cubic-using($cubic, function ($region as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, $exclusive) } ) }
Function: interpolate-cubic
declare function interpolate-cubic($n as xs:integer,
$cubic as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function interpolate-cubic($n as xs:integer, $cubic as map(xs:string,item()*)) as map(xs:string,item()*)*
Params
- n as xs:integer
- cubic as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:interpolate-cubic( $n as xs:integer, $cubic as map(xs:string,item()*) ) as map(xs:string,item()*)* { this:interpolate-cubic($n, $cubic, false()) }
Function: interpolate-arc-using
declare function interpolate-arc-using($arc as map(xs:string,item()*),
$divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
declare function interpolate-arc-using($arc as map(xs:string,item()*), $divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
interpolate-arc-using()
angle arc: break into sub-angles
point arc: point on circle => dest(center, angle, radius)
We only do angle interpolations; so convert to angle representation
However: the center and start/end to use depends on large and flipped
We reflect around the line between start/end points when we drawing on
the arc from the other circle through the points
Remapped degrees run from 0 to 360
We need to adjust the angles to account for running backwards and
for crossing 0
If flipped then reflect, unless we swapped, in which case reflect if we
aren't flipped.
Cases:
from=200 to=10 large => to-from = -190 swap; 10 to 200; reverse
from=200 to=40 large => to-from = -160 to+=360; 200 to 400
from=10 to=200 large => to-from = 190 10 to 200
from=40 to=200 large => to-from = 160 swap; from+=360; 200 to 400; reverse
from=200 to=10 !large => to-from = -190 to+=360; 200 to 370
from=200 to=40 !large => to-from = -160 swap; 40 to 200; reverse
from=10 to=200 !large => to-from = 190 swap; from+=360; 200 to 370; reverse
from=40 to=200 !large => to-from = 160 40 to 200
Params
- arc as map(xs:string,item()*)
- divisions as function(item())asxs:double*
Returns
- map(xs:string,item()*)*
declare function this:interpolate-arc-using( $arc as map(xs:string,item()*), $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $da := $to-angle - $from-angle return ( for $t in $divisions($arc) order by $t ascending return ( point:destination($center, $from-angle + $t * $da, $radius) ) ) }
Function: interpolate-arc
declare function interpolate-arc($n as xs:integer,
$arc as map(xs:string,item()*),
$exclusive as xs:boolean) as map(xs:string,item()*)*
declare function interpolate-arc($n as xs:integer, $arc as map(xs:string,item()*), $exclusive as xs:boolean) as map(xs:string,item()*)*
interpolate-arc()
Interpolate a arc edge.
Params
- n as xs:integer: number of interpolating points
- arc as map(xs:string,item()*) the edge
- exclusive as xs:boolean: exclude end point? default=false
Returns
- map(xs:string,item()*)*
declare function this:interpolate-arc( $n as xs:integer, $arc as map(xs:string,item()*), $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-arc-using($arc, function ($region as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, $exclusive) } ) }
Function: interpolate-arc
declare function interpolate-arc($n as xs:integer,
$arc as map(xs:string,item()*)) as map(xs:string,item()*)*
declare function interpolate-arc($n as xs:integer, $arc as map(xs:string,item()*)) as map(xs:string,item()*)*
Params
- n as xs:integer
- arc as map(xs:string,item()*)
Returns
- map(xs:string,item()*)*
declare function this:interpolate-arc( $n as xs:integer, $arc as map(xs:string,item()*) ) as map(xs:string,item()*)* { this:interpolate-arc($n, $arc, false()) }
Function: interpolate-using
declare function interpolate-using($edges as map(xs:string,item()*)*,
$divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
declare function interpolate-using($edges as map(xs:string,item()*)*, $divisions as function(item()) as xs:double*) as map(xs:string,item()*)*
Params
- edges as map(xs:string,item()*)*
- divisions as function(item())asxs:double*
Returns
- map(xs:string,item()*)*
declare function this:interpolate-using( $edges as map(xs:string,item()*)*, $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { for $edge in $edges return switch (this:kind($edge)) case "edge" return this:interpolate-edge-using($edge, $divisions) case "arc" return this:interpolate-arc-using($edge, $divisions) case "quad" return this:interpolate-quadratic-using($edge, $divisions) case "cubic" return this:interpolate-cubic-using($edge, $divisions) default return $edge }
Function: interpolate
declare function interpolate($n as xs:integer,
$edges as map(xs:string,item()*)*,
$exclusive as xs:boolean) as map(xs:string,item()*)*
declare function interpolate($n as xs:integer, $edges as map(xs:string,item()*)*, $exclusive as xs:boolean) as map(xs:string,item()*)*
interpolate()
Interpolate the edges; return the interpolated points.
Params
- n as xs:integer: number of points of interpolation for each edge
- edges as map(xs:string,item()*)*
- exclusive as xs:boolean: include end point? default=false
Returns
- map(xs:string,item()*)*
declare function this:interpolate( $n as xs:integer, $edges as map(xs:string,item()*)*, $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-using($edges, if ($exclusive) then ( function ($edge as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, true()) } ) else ( function ($edge as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1) } ) ) }
Function: interpolate
declare function interpolate($n as xs:integer,
$edges as map(xs:string,item()*)*) as map(xs:string,item()*)*
declare function interpolate($n as xs:integer, $edges as map(xs:string,item()*)*) as map(xs:string,item()*)*
Params
- n as xs:integer
- edges as map(xs:string,item()*)*
Returns
- map(xs:string,item()*)*
declare function this:interpolate( $n as xs:integer, $edges as map(xs:string,item()*)* ) as map(xs:string,item()*)* { this:interpolate($n, $edges, false()) }
Function: is-straight-edge
declare function is-straight-edge($edge as map(xs:string,item()*)) as xs:boolean
declare function is-straight-edge($edge as map(xs:string,item()*)) as xs:boolean
is-straight-edge()
Test whether the edge is straight: either a linear edge or degenerate.
Params
- edge as map(xs:string,item()*)
Returns
- xs:boolean
declare function this:is-straight-edge( $edge as map(xs:string,item()*) ) as xs:boolean { switch (this:kind($edge)) case "edge" return true() case "quad" return point:sorientation(this:start($edge), this:controls($edge), this:end($edge))=0 or this:length($edge) = 0 case "cubic" return (point:sorientation(this:start($edge), this:controls($edge)[1], this:end($edge))=0 and point:sorientation(this:start($edge), this:controls($edge)[2], this:end($edge))=0) or this:length($edge) = 0 case "arc" return this:length($edge) = 0 default return errors:error("GEOM-BADREGION", ($edge, "is-straight-edge")) }
Function: is-straight
declare function is-straight($edges as map(xs:string,item()*)*) as xs:boolean
declare function is-straight($edges as map(xs:string,item()*)*) as xs:boolean
is-straight()
Does the sequence of edges align?
Params
- edges as map(xs:string,item()*)*
Returns
- xs:boolean
declare function this:is-straight($edges as map(xs:string,item()*)*) as xs:boolean { let $n := count($edges) return ( this:is-straight-edge(head($edges)) and ( every $i in 2 to $n satisfies ( this:is-straight-edge($edges[$i]) and point:sorientation(this:start($edges[$i - 1]), this:start($edges[$i]), this:end($edges[$i]))=0 ) ) ) }
Function: map-command
declare function map-command($kind as xs:string?,
$relative as xs:string?) as xs:string
declare function map-command($kind as xs:string?, $relative as xs:string?) as xs:string
Params
- kind as xs:string?
- relative as xs:string?
Returns
- xs:string
declare function this:map-command( $kind as xs:string?, $relative as xs:string? ) as xs:string { switch ($relative) case "relative" return ( switch ($kind) case "goto" return "m" case "close" return "z" case "line" return "l" case "hr" return "h" case "vr" return "v" case "cubic" return "c" case "smooth_cubic" return "s" case "quad" return "q" case "smooth_quad" return "t" case "arc" return "a" case "ellipse-arc" return "a" default (: line :) return "l" ) default (: absolute :) return ( switch ($kind) case "goto" return "M" case "close" return "Z" case "line" return "L" case "hr" return "H" case "vr" return "V" case "cubic" return "C" case "smooth_cubic" return "S" case "quad" return "Q" case "smooth_quad" return "T" case "arc" return "A" case "ellipse-arc" return "A" default (: line :) return "L" ) }
Function: translate-edge
declare function translate-edge($edge as map(xs:string,item()*)) as xs:string
declare function translate-edge($edge as map(xs:string,item()*)) as xs:string
Params
- edge as map(xs:string,item()*)
Returns
- xs:string
declare function this:translate-edge( $edge as map(xs:string,item()*) ) as xs:string { string-join(( this:map-command(($edge("variety"),$edge("kind"))[1],"absolute"), switch(this:kind($edge)) case "arc" return ( (: XYZZY should swap start and end for large to get proper intent :) this:arc-radius($edge), (: rx :) this:arc-radius($edge), (: ry :) 0, (: rotate :) if (this:arc-large($edge)) then 1 else 0, (: large-arc :) if (this:arc-flipped($edge)) then 0 else 1 (: sweep :) ) case "ellipse-arc" return ( $edge("rx"), (: rx :) $edge("ry"), (: ry :) 0, (:rotate :) if (this:arc-large($edge)) then 1 else 0, (: large-arc :) if (this:arc-flipped($edge)) then 0 else 1 (: sweep :) ) case "quad" return ( point:px(this:controls($edge)[1]), (: c1.x :) point:py(this:controls($edge)[1]) (: c1.y :) ) case "cubic" return ( point:px(this:controls($edge)[1]), (: c1.x :) point:py(this:controls($edge)[1]), (: c1.y :) point:px(this:controls($edge)[2]), (: c2.x :) point:py(this:controls($edge)[2]) (: c2.y :) ) default return () , point:px(this:end($edge)), point:py(this:end($edge)) )!string(.)," ") }
Function: draw
declare function draw($item as map(xs:string,item()*),
$properties as map(xs:string,item()*),
$drawing as map(xs:string,function(*)?)) as item()*
declare function draw($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?)) as item()*
Params
- item as map(xs:string,item()*)
- properties as map(xs:string,item()*)
- drawing as map(xs:string,function(*)?)
Returns
- item()*
declare function this:draw( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { if ($config:DRAWING-METHOD="art") then ( switch(this:kind($item)) case "edge" return $this:draw-edge-impl($item, $properties, $drawing) case "quad" return $this:draw-quad-impl($item, $properties, $drawing) case "cubic" return $this:draw-cubic-impl($item, $properties, $drawing) case "arc" return $this:draw-arc-impl($item, $properties, $drawing) case "ellipse-arc" return $this:draw-ellipse-arc-impl($item, $properties, $drawing) default return () ) else ( $this:draw-svg-edge-impl($item, $properties, $drawing) ) }
Original Source Code
xquery version "3.1"; (:~ : Edges: including arcs, quads, and cubics : Edges are decorated with properties which art infrastructure may care : about for rendering purposes. : : Properties of note: : r: radius of arc for arc edges; default is none : width: width for stroke; default=d : label: label for this edge : class, colour, opacity: direct styling overrides : : Copyright© Mary Holstege 2020-2023 : CC-BY (https://creativecommons.org/licenses/by/4.0/) : @since April 2021 : @custom:Status Incomplete, subject to refactoring :) module namespace this="http://mathling.com/geometric/edge"; import module namespace config="http://mathling.com/core/config" at "../core/config.xqy"; import module namespace errors="http://mathling.com/core/errors" at "../core/errors.xqy"; import module namespace util="http://mathling.com/core/utilities" at "../core/utilities.xqy"; import module namespace v="http://mathling.com/core/vector" at "../core/vector.xqy"; import module namespace roots="http://mathling.com/core/roots" at "../core/roots.xqy"; import module namespace point="http://mathling.com/geometric/point" at "../geo/point.xqy"; import module namespace affine="http://mathling.com/geometric/affine" at "../geo/affine.xqy"; import module namespace box="http://mathling.com/geometric/box" at "../geo/box.xqy"; import module namespace ellipse="http://mathling.com/geometric/ellipse" at "../geo/ellipse.xqy"; declare namespace art="http://mathling.com/art"; declare namespace svg="http://www.w3.org/2000/svg"; declare namespace map="http://www.w3.org/2005/xpath-functions/map"; declare namespace array="http://www.w3.org/2005/xpath-functions/array"; declare namespace math="http://www.w3.org/2005/xpath-functions/math"; declare variable $this:precision as xs:integer := math:log10(1 div $config:ε) cast as xs:integer; declare variable $this:EDGE-RESERVED as xs:string* := ("kind", "u", "v", "d"); (:~ : edge() : Make an edge : : @param $from: starting point : @param $to: ending point : @param $d: weight of the edge (used by spanning graph as distance) : @param $properties: additional edge properties :) declare function this:edge( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*) ) as map(xs:string,item()*) { util:merge-into($properties, map { "kind": "edge", "u": $from, "v": $to, "d": $d } ) }; (:~ : edge() : Make an edge : : @param $from: starting point : @param $to: ending point : @param $d: the weight of the edge :) declare function this:edge( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $d as xs:double ) as map(xs:string,item()*) { map { "kind": "edge", "u": $from, "v": $to, "d": $d } }; (:~ : edge() : Make an edge : : @param $from: starting point : @param $to: ending point :) declare function this:edge( $from as map(xs:string,item()*), $to as map(xs:string,item()*) ) as map(xs:string,item()*) { this:edge($from, $to, 0) }; declare function this:kind($edge as map(xs:string,item()*)) as xs:string { $edge("kind") }; (:~ : start() : Accessor for starting point of edge : : @param $edge: The edge :) declare function this:start($edge as map(xs:string,item()*)) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return $edge("u") case "quad" return $edge("u") case "cubic" return $edge("u") case "arc" return ( let $start := $edge("start") return if (exists($start)) then $start else ( let $circle := $edge("circle") let $center := ellipse:center($circle) let $r := ellipse:radius($circle) let $from := util:radians($edge("from-angle")) return ( point:point( point:px($center)+math:cos($from)*$r, point:py($center)+math:sin($from)*$r ) ) ) ) case "ellipse-arc" return $edge("start") default return errors:error("GEOM-BADREGION", ($edge, "start")) }; (:~ : start() : Settor for starting point of edge : : @param $edge: The edge : @param $start: Starting point of edge :) declare function this:start($edge as map(xs:string,item()*), $start as map(xs:string,item()*)) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return $edge=>map:put("u", $start) case "quad" return $edge=>map:put("u", $start) case "cubic" return $edge=>map:put("u", $start) case "arc" return this:as-point-arc($edge)=>map:put("start", $start) case "ellipse-arc" return $edge=>map:put("start", $start) default return errors:error("GEOM-BADREGION", ($edge, "start")) }; (:~ : end() : Accessor for ending point of edge : : @param $edge: The edge :) declare function this:end($edge as map(xs:string,item()*)) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return $edge("v") case "quad" return $edge("v") case "cubic" return $edge("v") case "arc" return ( let $end := $edge("end") return if (exists($end)) then $end else ( let $circle := $edge("circle") let $center := ellipse:center($circle) let $r := ellipse:radius($circle) let $to := util:radians($edge("to-angle")) return point:point( point:px($center)+math:cos($to)*$r, point:py($center)+math:sin($to)*$r ) ) ) case "ellipse-arc" return $edge("end") default return errors:error("GEOM-BADREGION", ($edge, "end")) }; (:~ : weight() : Accessor for the weight of an edge : : @param $edge: The edge :) declare function this:weight($edge as map(xs:string,item()*)) as xs:double { if (this:kind($edge) = ("edge","cubic","quad")) then ($edge("d"),0)[1] else 0 }; declare variable $this:ARC-RESERVED as xs:string* := ("kind", "circle", "start", "end", "from-angle", "to-angle", "flipped", "large") ; (:~ : arc() : Construct an arc (e.g. hyperbolic edge) : Note: calculations on $start and $end need to be correct: : use hyperbolic:arc-through() to get them right; this is really an : internal contructor function. Ad hoc construction of arc should use : arc-by-angle(). : : @param $center: center of curvature : @param $radius: radius of circle : @param $start: starting point : @param $end: ending point : @param $flipped: indicates sweep : @param $large: indicated large arc :) declare function this:arc( $center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean, $large as xs:boolean ) as map(xs:string,item()*) { map { "kind": "arc", "circle": ellipse:circle($center, $radius), "start": $start, "end": $end, "flipped": $flipped, "large": $large } }; declare function this:arc( $center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean ) as map(xs:string,item()*)* { this:arc($center, $radius, $start, $end, $flipped, false()) }; declare function this:arc( $center as map(xs:string,item()*), $radius as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*) ) as map(xs:string,item()*) { this:arc($center, $radius, $start, $end, false(), false()) }; (:~ : arc-by-angle() : Construct an arc : : @param $center: center of curvature : @param $radius: radius of circle : @param $θ: starting angle (degrees) : @param $θ2: ending angle (degrees) : @param $flipped: indicates sweep : @param $large: indicates large arc :) declare function this:arc-by-angle( $center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double, $flipped as xs:boolean, $large as xs:boolean ) as map(xs:string,item()*) { map { "kind": "arc", "circle": ellipse:circle($center, $radius), "from-angle": $θ, "to-angle": $θ2, "flipped": $flipped, "large": $large } }; declare function this:arc-by-angle( $center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double, $flipped as xs:boolean ) as map(xs:string,item()*) { this:arc-by-angle($center, $radius, $θ, $θ2, $flipped, false()) }; declare function this:arc-by-angle( $center as map(xs:string,item()*), $radius as xs:double, $θ as xs:double, $θ2 as xs:double ) as map(xs:string,item()*) { this:arc-by-angle($center, $radius, $θ, $θ2, false(), false()) }; (:~ : arc-circle() : Center and radius of curvature for this arc : $arc: The arc :) declare function this:arc-circle($arc as map(xs:string,item()*)) as map(xs:string,item()*) { $arc("circle") }; (:~ : arc-center() : Center of this arc : $arc: The arc :) declare function this:arc-center($arc as map(xs:string,item()*)) as map(xs:string,item()*) { if (this:kind($arc)="ellipse-arc") then $arc("ellipse")=>ellipse:center() else $arc("circle")=>ellipse:center() }; (:~ : arc-radius() : Radius of curvature of this arc : $arc: The arc :) declare function this:arc-radius($arc as map(xs:string,item()*)) as xs:double { $arc("circle")=>ellipse:radius() }; (:~ : arc-angles() : Starting and ending angles for the arc (degrees), for arc-by-angle arcs : $arc: The arc :) declare function this:arc-angles($arc as map(xs:string,item()*)) as xs:double* { ($arc("from-angle"),$arc("to-angle")) }; (:~ : arc-ends() : Starting and ending points for the arc, for arc-by-point arcs : $arc: The arc :) declare function this:arc-ends($arc as map(xs:string,item()*)) as map(xs:string,item()*)* { ($arc("start"),$arc("end")) }; (:~ : arc-flipped() : Sweep for the arc : $arc: The arc :) declare function this:arc-flipped($arc as map(xs:string,item()*)) as xs:boolean { (: Default is false :) if ($arc("flipped")) then true() else false() }; declare function this:arc-large($arc as map(xs:string,item()*)) as xs:boolean { (: Default is false :) if ($arc("large")) then true() else false() }; (:~ : as-angle-arc() : Shift arc edge from the end-point representation to the angle representation. : : @param $arc: input arc :) declare function this:as-angle-arc($arc as map(xs:string,item()*)) as map(xs:string,item()*) { let $points := $arc=>this:arc-ends() return ( if (exists($points)) then ( let $circle := $arc=>this:arc-circle() let $center := ellipse:center($circle) let $α := this:angle($center, $points[1]) let $ω := this:angle($center, $points[2]) return util:merge-into( this:property-map($arc), this:arc-by-angle( $center, ellipse:radius($circle), $α, if ($ω - $α > 360) then 360 - $ω else $ω, $arc=>this:arc-flipped(), $arc=>this:arc-large() ) ) ) else ( $arc ) ) }; (:~ : as-point-arc() : Shift arc edge from the angle representation to the end-point representation. : : @param $arc: input arc :) declare function this:as-point-arc( $arc as map(xs:string,item()*) ) as map(xs:string,item()*) { let $points := $arc=>this:arc-ends() return ( if (exists($points)) then ( $arc ) else ( let $circle := $arc=>this:arc-circle() return ( util:merge-into( this:property-map($arc), this:arc( ellipse:center($circle), ellipse:radius($circle), $arc=>this:start(), $arc=>this:end(), $arc=>this:arc-flipped(), $arc=>this:arc-large() ) ) ) ) ) }; (: ellipse arc: right now all you can do with this draw it :) declare variable $this:ELLIPSE-ARC-RESERVED as xs:string* := ("kind", "ellipse", "start", "end", "flipped", "large") ; declare function this:ellipse-arc( $center as map(xs:string,item()*), $a as xs:double, $b as xs:double, $rotation as xs:double, $start as map(xs:string,item()*), $end as map(xs:string,item()*), $flipped as xs:boolean, $large as xs:boolean ) { map { "kind": "ellipse-arc", "ellipse": ellipse:ellipse($center, $a, $b, $rotation), "start": $start, "end": $end, "flipped": $flipped, "large": $large } }; declare function this:arc-ellipse($ellipse-arc as map(xs:string,item()*)) as map(xs:string,item()*) { $ellipse-arc("ellipse") }; declare variable $this:QUAD-RESERVED as xs:string* := ($this:EDGE-RESERVED, "c1"); (:~ : quad() : Construct a quadratic edge. : : @param $from: starting point : @param $to: ending point : @param $control: control point : @param $d: edge weight : @param $properties: additional properties :) declare function this:quad( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*) ) as map(xs:string,item()*) { util:merge-into($properties, map { "kind": "quad", "u": $from, "v": $to, "c1": $control, "d": $d } ) }; declare function this:quad( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*), $d as xs:double ) as map(xs:string,item()*) { map { "kind": "quad", "u": $from, "v": $to, "c1": $control, "d": $d } }; declare function this:quad( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control as map(xs:string,item()*) ) as map(xs:string,item()*) { this:quad($from, $to, $control, 0) }; (:~ : controls() : Get control points from a quadratic or cubic edge. :) declare function this:controls( $edge as map(xs:string,item()*) ) as map(xs:string,item()*)* { switch(this:kind($edge)) case "quad" return $edge("c1") case "cubic" return ($edge("c1"),$edge("c2")) default return errors:error("GEOM-BADREGION", ($edge, "controls")) }; declare variable $this:CUBIC-RESERVED as xs:string* := ($this:EDGE-RESERVED, "c1", "c2"); (:~ : cubic() : Construct a cubic edge. : : @param $from: starting point : @param $to: ending point : @param $control1: first control point : @param $control2: second control point : @param $d: edge weight : @param $properties: additional properties :) declare function this:cubic( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*), $d as xs:double, $properties as map(xs:string,item()*) ) as map(xs:string,item()*) { util:merge-into($properties, map { "kind": "cubic", "u": $from, "v": $to, "c1": $control1, "c2": $control2, "d": $d } ) }; declare function this:cubic( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*), $d as xs:double ) as map(xs:string,item()*) { map { "kind": "cubic", "u": $from, "v": $to, "c1": $control1, "c2": $control2, "d": $d } }; declare function this:cubic( $from as map(xs:string,item()*), $to as map(xs:string,item()*), $control1 as map(xs:string,item()*), $control2 as map(xs:string,item()*) ) as map(xs:string,item()*) { this:cubic($from, $to, $control1, $control2, 0) }; (:~ : points() : Get 2D points from the region :) declare function this:points($region as map(xs:string,item()*)) as map(xs:string,item()*)* { this:vertices($region)!point:as-dimension(.,2) }; (:~ : vertices() : Get all the region's points. :) declare function this:vertices($region as map(xs:string,item()*)) as map(xs:string,item()*)* { switch(this:kind($region)) case "edge" return ($region=>this:start(), $region=>this:end()) case "arc" return ($region=>this:start(), $region=>this:end()) case "quad" return ($region=>this:start(), $region=>this:end()) case "cubic" return ($region=>this:start(), $region=>this:end()) default return errors:error("GEOM-BADREGION", ($region, "vertices")) }; (:~ : to-edges() : Convert a sequence of points into a sequence of edges. : : @param $points: the sequence of points :) declare function this:to-edges($points as map(xs:string,item()*)*) as map(xs:string,item()*)* { for $pt at $i in tail($points) return this:edge($points[$i], $pt) }; (:~ : to-edges() : Convert a sequence of points into a sequence of edges. : : @param $points: the sequence of points : @param $properties: edge properties :) declare function this:to-edges($points as item()*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)* { let $d := ($properties("d"),0)[1] (: Special case for d :) for $pt at $i in tail($points) return this:edge($points[$i], $pt, $d, $properties) }; (:~ : skip() : Make a skip edge (pure goto) : : @param $from: starting point : @param $to: ending point :) declare function this:skip( $from as map(xs:string,item()*), $to as map(xs:string,item()*) ) as map(xs:string,item()*) { this:edge($from, $to)=>map:put("variety", "goto") }; (:====================================================================== : Property management :======================================================================:) (:~ : property-map() : Return the annotation properties of the edge as a map. Check whether this : is actually an edge. : : @param $region: the region :) declare function this:property-map( $region as map(xs:string,item()*) ) as map(xs:string,item()*) { switch(this:kind($region)) case "edge" return util:exclude($region,$this:EDGE-RESERVED) case "arc" return util:exclude($region,$this:ARC-RESERVED) case "quad" return util:exclude($region, $this:QUAD-RESERVED) case "cubic" return util:exclude($region, $this:CUBIC-RESERVED) case "ellipse-arc" return util:exclude($region, $this:ELLIPSE-ARC-RESERVED) default return map {} }; (:~ : properties() : Return the names of the annotation properties of the edge. : Check whether this is actually an edge : : @param $region: the region :) declare function this:properties( $region as map(xs:string,item()*) ) as xs:string* { switch(this:kind($region)) case "edge" return ($region=>map:keys())[not(. = $this:EDGE-RESERVED)] case "arc" return ($region=>map:keys())[not(. = $this:ARC-RESERVED)] case "quad" return ($region=>map:keys())[not(. = $this:QUAD-RESERVED)] case "cubic" return ($region=>map:keys())[not(. = $this:CUBIC-RESERVED)] case "ellipse-arc" return ($region=>map:keys())[not(. = $this:ELLIPSE-ARC-RESERVED)] default return () }; (:~ : with-properties() : Annotate the edge with some new properties and return the new edge. : Will not touch any of the core properties. Will override existing properties : with the same keys but leave properties with different keys in place. : Raises an error if this is not actually an edge. : : @param $region: the region :) declare function this:with-properties( $region as map(xs:string,item()*), $properties as map(xs:string,item()*) ) as map(xs:string,item()*) { switch(this:kind($region)) case "edge" return util:merge-into($region, util:exclude($properties,$this:EDGE-RESERVED)) case "arc" return util:merge-into($region, util:exclude($properties,$this:ARC-RESERVED)) case "quad" return util:merge-into($region, util:exclude($properties, $this:QUAD-RESERVED)) case "cubic" return util:merge-into($region, util:exclude($properties, $this:CUBIC-RESERVED)) case "ellipse-arc" return util:merge-into($region, util:exclude($properties,$this:ELLIPSE-ARC-RESERVED)) default return errors:error("GEOM-BADREGION", ($region, "with-properties")) }; (:====================================================================== : Operations :======================================================================:) (:~ : snap() : Snap the coordinates of the points, returning the edges with snapped : (i.e. integer) points coordinates. : : @param $edges the edges :) declare function this:snap( $edges as map(xs:string,item()*)* ) as map(xs:string,item()*)* { for $edge in $edges return switch(this:kind($edge)) case "edge" return this:edge( point:snap(this:start($edge)), point:snap(this:end($edge)), this:weight($edge), $edge ) case "quad" return this:quad( point:snap(this:start($edge)), point:snap(this:end($edge)), point:snap(this:controls($edge)[1]), this:weight($edge), $edge ) case "cubic" return let $controls := this:controls($edge) return this:cubic( point:snap(this:start($edge)), point:snap(this:end($edge)), point:snap($controls[1]), point:snap($controls[2]), this:weight($edge), $edge ) case "arc" return let $circle := $edge=>this:arc-circle() let $points := $edge=>this:arc-ends() let $angles := $edge=>this:arc-angles() return if (empty($points)) then ( util:assert(exists($angles),"Arc must have points or angles"), $edge=> map:put("circle", ellipse:snap($circle)) ) else ( $edge=> map:put("circle", ellipse:snap($circle))=> map:put("start", point:snap($points[1]))=> map:put("end", point:snap($points[2])) ) case "ellipse-arc" return let $ellipse := $edge=>this:arc-ellipse() let $points := $edge=>this:arc-ends() return $edge=> map:put("ellipse", ellipse:snap($ellipse))=> map:put("start", point:snap($points[1]))=> map:put("end", point:snap($points[2])) default return $edge }; (:~ : decimal() : Perform decimal rounding on all the point coordinates (see util:decimal). : : @param $edges: the edges to round : @param $digits: how many digits after the decimal point to keep :) declare function this:decimal( $edges as map(xs:string,item()*)*, $digits as xs:integer ) as map(xs:string,item()*)* { for $edge in $edges return switch(this:kind($edge)) case "edge" return this:edge( point:decimal(this:start($edge), $digits), point:decimal(this:end($edge), $digits), this:weight($edge), $edge ) case "quad" return this:quad( point:decimal(this:start($edge), $digits), point:decimal(this:end($edge), $digits), point:decimal(this:controls($edge)[1], $digits), this:weight($edge), $edge ) case "cubic" return let $controls := this:controls($edge) return this:cubic( point:decimal(this:start($edge), $digits), point:decimal(this:end($edge), $digits), point:decimal($controls[1], $digits), point:decimal($controls[2], $digits), this:weight($edge), $edge ) case "arc" return let $circle := $edge=>this:arc-circle() let $points := $edge=>this:arc-ends() let $angles := $edge=>this:arc-angles() return if (empty($points)) then ( util:assert(exists($angles),"Arc must have points or angles"), $edge=> map:put("circle", ellipse:decimal($circle, $digits)) ) else ( $edge=> map:put("circle", ellipse:decimal($circle, $digits))=> map:put("start", point:decimal($points[1], $digits))=> map:put("end", point:decimal($points[2], $digits)) ) case "ellipse-arc" return let $ellipse := $edge=>this:arc-ellipse() let $points := $edge=>this:arc-ends() return $edge=> map:put("ellipse", ellipse:decimal($ellipse, $digits))=> map:put("start", point:decimal($points[1], $digits))=> map:put("end", point:decimal($points[2], $digits)) default return $edge }; (:~ : quote() : Return a string value for the edges, suitable for debugging. : : @param $edges: the edge sequence to quote :) declare function this:quote( $edges as map(xs:string,item()*)* ) as xs:string { string-join( for $edge in $edges return switch(this:kind($edge)) case "edge" return point:quote($edge("u"))||":"||point:quote($edge("v")) case "quad" return point:quote($edge("u"))||":"||point:quote($edge("v"))||"<"||point:quote($edge("c1"))||">" case "cubic" return point:quote($edge("u"))||":"||point:quote($edge("v"))||"<"||point:quote($edge("c1"))||";"||point:quote($edge("c2"))||">" case "arc" return "("|| ellipse:quote($edge("circle"))|| "<"|| point:quote($edge("start"))|| $edge("from-angle")||":"|| point:quote($edge("end"))|| $edge("to-angle")|| ">"|| (if ($edge("large")) then "+l1" else "+l0")|| (if ($edge("flipped")) then "+f1" else "+f0")|| ")" case "ellipse-arc" return "("|| ellipse:quote($edge("ellipse"))|| "<"|| point:quote($edge("start"))|| point:quote($edge("end"))|| ">"|| (if ($edge("large")) then "+l1" else "+l0")|| (if ($edge("flipped")) then "+f1" else "+f0")|| ")" default return errors:quote($edge) , " " ) }; (:~ : same() : Equality comparison for edges, ignoring annotation properties. : Return true() if they have equal coordinates. : : @param $this: one edge : @param $other: the edge to compare it to :) declare function this:same( $this as map(xs:string,item()*), $other as map(xs:string,item()*) ) as xs:boolean { let $this-kind := this:kind($this) let $other-kind := this:kind($other) return ( ($this-kind=$other-kind) and ( switch($this-kind) case "arc" return (this:arc-large($this) = this:arc-large($other)) and (this:arc-flipped($this) = this:arc-flipped($other)) and ellipse:same($this("circle"),$other("circle")) and ( if (empty($this("start"))) then empty($other("start")) else if (empty($other("start"))) then false() else point:same($this("start"), $other("start")) ) and ( if (empty($this("end"))) then empty($other("end")) else if (empty($other("end"))) then false() else point:same($this("end"), $other("end")) ) and ( if (empty($this("from-angle"))) then empty($other("from-angle")) else if (empty($other("from-angle"))) then false() else $this("from-angle")=$other("from-angle") ) and ( if (empty($this("to-angle"))) then empty($other("to-angle")) else if (empty($other("to-angle"))) then false() else $this("to-angle")=$other("to-angle") ) case "edge" return point:same($this("u"),$other("u")) and point:same($this("v"),$other("v")) case "quad" return point:same($this("u"),$other("u")) and point:same($this("v"),$other("v")) and point:same($this("c1"),$other("c1")) case "cubic" return point:same($this("u"),$other("u")) and point:same($this("v"),$other("v")) and point:same($this("c1"),$other("c1")) and point:same($this("c2"),$other("c2")) case "ellipse-arc" return (this:arc-large($this) = this:arc-large($other)) and (this:arc-flipped($this) = this:arc-flipped($other)) and ellipse:same($this("ellipse"),$other("ellipse")) and point:same($this("start"), $other("start")) and point:same($this("end"), $other("end")) default return deep-equal($this,$other) ) ) }; (:~ : mutate() : Run a function over a sequence of edges to produce a new sequence of : edges. The function maps points to points. : : @param $regions: input sequence of edges : @param $mutate: function that takes a point as an argument and returns a new point :) declare function this:mutate( $regions as map(xs:string,item()*)*, $mutate as function(map(xs:string,item()*)) as map(xs:string,item()*) (: point to point :) ) as map(xs:string,item()*)* { for $edge in $regions return ( switch (this:kind($edge)) case "edge" return ( this:edge( $mutate($edge=>this:start()), $mutate($edge=>this:end()), $edge=>this:weight(), $edge ) ) case "cubic" return ( let $controls := ($edge=>this:controls())!$mutate(.) return this:cubic( $mutate($edge=>this:start()), $mutate($edge=>this:end()), $controls[1], $controls[2], $edge=>this:weight(), $edge ) ) case "quad" return ( let $control := $mutate(this:controls($edge)[1]) return this:quad( $mutate($edge=>this:start()), $mutate($edge=>this:end()), $control, $edge=>this:weight(), $edge ) ) case "arc" return ( let $points := $edge=>this:arc-ends() let $angles := $edge=>this:arc-angles() return ( if (exists($points)) then ( this:arc( $mutate($edge=>this:arc-center()), $edge=>this:arc-radius(), $mutate($edge=>this:start()), $mutate($edge=>this:end()), $edge=>this:arc-flipped(), $edge=>this:arc-large() ) ) else ( this:arc-by-angle( $mutate($edge=>this:arc-center()), $edge=>this:arc-radius(), $angles[1], $angles[2], $edge=>this:arc-flipped(), $edge=>this:arc-large() ) ) )=>this:with-properties($edge) ) default return $edge ) }; (:~ : reverse() : Reverse the direction of the edge. :) declare function this:reverse($edge as map(xs:string,item()*)) as map(xs:string,item()*) { switch(this:kind($edge)) case "edge" return this:edge( this:end($edge), this:start($edge), this:weight($edge), $edge ) case "quad" return this:quad( this:end($edge), this:start($edge), this:controls($edge)[1], this:weight($edge), $edge ) case "cubic" return this:cubic( this:end($edge), this:start($edge), this:controls($edge)[2], this:controls($edge)[1], this:weight($edge), $edge ) case "arc" return ( let $circle := $edge=>this:arc-circle() let $points := $edge=>this:arc-ends() let $angles := $edge=>this:arc-angles() return ( if (exists($points)) then ( this:arc( $circle=>ellipse:center(), $circle=>ellipse:radius(), $points[2], $points[1], not($edge=>this:arc-flipped()), $edge=>this:arc-large() ) ) else ( this:arc-by-angle( $circle=>ellipse:center(), $circle=>ellipse:radius(), $angles[2], $angles[1], not($edge=>this:arc-flipped()), $edge=>this:arc-large() ) ) ) ) default return $edge }; (:====================================================================== : Edge calculations :======================================================================:) (:~ : angle() : Bearing of the edge between start and end points. : : @param $edge: the edge :) declare function this:angle($edge as map(xs:string,item()*)) as xs:double { this:angle(this:start($edge), this:end($edge)) }; (:~ : angle() : Compute the angle (azimuth) from one point to the next, in degrees : Return 0 if points are the same : : @param $last: previous point; use (0,0) if no previous : @param $curr: current point :) declare function this:angle($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double { let $this := $curr=>point:as-dimension(2) let $prev := point:as-dimension(($last,$point:ORIGIN)[1],2) return if (point:same($prev,$this) or (point:distance($prev,$this) < $config:ε)) then 0 else if (point:px($this)=point:px($prev)) then ( if (point:py($prev) > point:py($this)) then 270 (: remapped -90 :) else 90 ) else ( util:remap-degrees(util:degrees( (math:pi() div 2) - math:atan2(point:px($this) - point:px($prev), point:py($this) - point:py($prev)) )) ) }; (:~ : inclination() : Inclination of the edge between start and end points. : : @param $edge: the edge :) declare function this:inclination($edge as map(xs:string,item()*)) as xs:double { this:inclination(this:start($edge), this:end($edge)) }; (:~ : inclination() : Compute the inclination angle from one point in the next, in degrees : Return 90 if the points are the same : : @param $last: previous point; use (0,0,0) if no previous : @param $curr: current point :) declare function this:inclination($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double { let $curr := $curr=>point:as-dimension(3) let $prev := (($last,$point:ORIGIN3D)[1])=>point:as-dimension(3) let $d := point:distance($prev,$curr) return if (point:same($prev,$curr) or ($d < $config:ε)) then 90 else ( util:remap-degrees(util:degrees( math:acos( (point:pz($curr) - point:pz($prev)) div $d ) )) ) }; (:~ : linear-point() : Compute the point on the edge the given fraction from the start : : @param $edge: the edge : @param $t: fraction of distance from start to end (t) [0,1] :) declare function this:linear-point($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { let $from := $edge=>this:start() let $to := $edge=>this:end() return ( point:map2(function ($a as xs:double, $b as xs:double) as xs:double {$a + ($b - $a)*$t}, $from, $to) ) }; (:~ : quad-point() : Compute the point on the quad edge the given fraction from the start : Midpoint of quad edge is quad-point(0.5) : B(t)=(1-t)²P0 + 2(1-t)tP1 + t²P2, 0 <= t <= 1 : : @param $quadratic: the edge : @param $t: fraction of distance from start to end (t) [0,1] :) declare function this:quad-point($quadratic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { let $from := $quadratic=>this:start() let $to := $quadratic=>this:end() let $controls := $quadratic=>this:controls() return ( point:map3( function ($a as xs:double, $b as xs:double, $c as xs:double) as xs:double { (1 - $t)*(1 - $t)*$a + 2*(1 - $t)*$t*$c + $t * $t * $b }, $from, $to, $controls[1] ) ) }; (:~ : cubic-point() : Compute the point on the cubic edge the given fraction from the start : Midpoint of cubic edge is cubic-point(0.5) : B(t)=(1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3, 0 <= t <= 1 : : @param $cubic: the edge : @param $t: fraction of distance from start to end (t) [0,1] :) declare function this:cubic-point($cubic as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { let $from := $cubic=>this:start() let $to := $cubic=>this:end() let $controls := $cubic=>this:controls() return ( point:map4( function ($a as xs:double, $b as xs:double, $c as xs:double, $d as xs:double) as xs:double { (1 - $t)*(1 - $t)*(1 - $t)*$a + 3*(1 - $t)*(1 - $t)*$t*$c + 3*(1 - $t)*$t*$t*$d + $t*$t*$t*$b }, $from, $to, $controls[1], $controls[2] ) ) }; (:~ : arc-point() : Compute the point on the arc edge the given fraction from the start : Midpoint of arc edge is arc-point(0.5) : Will normalize arc properly. : : @param $arc: the edge : @param $t: fraction of distance from start to end (t) [0,1] :) declare function this:arc-point($arc as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $da := $to-angle - $from-angle return point:destination($center, $from-angle + $t*$da, $radius) }; (:~ : length() : Travel distance along edge. : Cubic is an approximation. :) declare function this:length($edge as map(xs:string,item()*)) as xs:double { switch (this:kind($edge)) case "arc" return this:arc-length($edge) case "quad" return this:quad-length($edge) case "cubic" return this:cubic-length($edge) (: approximation by 10 slices :) case "edge" return this:linear-length($edge) default return errors:error("GEOM-BADREGION", ($edge, "length")) }; (:~ : linear-length() : Crow-flies distance between end points of an edge. Same a travel distance : along the edge if it is a linear edge. : : @param $edge: the edge :) declare function this:linear-length($edge as map(xs:string,item()*)) as xs:double { point:distance($edge=>this:start(), $edge=>this:end()) }; (:~ : quad-length() : Length of travel along a quadratic edge. : : @param $quad: the quadratic edge : : Derivation, see: https://members.loria.fr/SHornus/quadratic-arc-length.html : : B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2 : Reformulate to: P(t) = At² + 2Bt + C where : A = P0 - P1 + P2 - P1 : B = P1 - P0 : C = P0 : let F = P2 - P1, so A = F - B : length = : (|F|(A · F) - |B|(A · B)) / |A|² + : (|B|²/|A| - (A · B)²/|A|³) X (log(|A||F| + A · F) - log(|A||B| + A · B)) :) declare function this:quad-length($quad as map(xs:string,item()*)) as xs:double { let $P0 := this:start($quad) let $P1 := this:controls($quad)[1] let $P2 := this:end($quad) let $F := point:sub($P2, $P1) let $B := point:sub($P1, $P0) let $A := point:sub($F, $B) let $nA := point:magnitude($A) let $nB := point:magnitude($B) let $nF := point:magnitude($F) let $A.B := point:dot($A, $B) let $A.F := point:dot($A, $F) return ( (: Possible error cases : nA=0 => F=B => start, c0, end are colinear => a straight edge : : nA*nF + A.F = 0 => nA*nF = -A.F => log(nA*nF + A.F) = -INF : nA*nF= √(Σai*ai)√(Σfi*fi) A.F=Σ(ai*fi) : A = F - B => nA*nF = √(Σ(fi-bi)*(fi-bi))√(Σfi*fi) A.F=Σ((fi-bi)*fi) => det(B,F) = 0 => : B and F are colinear => a straight edge again : : nA*nB + A.B = 0 => log(nA*nA + A.B) = -INF => nA*nB = -A.B => nA*nA*nB*nB = A.B*A.B => : (nB*nB / nA) - (A.B*A.B)/(nA*nA*nA) = 0 => treat 0*-INF as 0 :) if ($nA*$nA = 0) then ( point:distance($P0, $P2) ) else if ($nA*$nB + $A.B = 0) then ( ($nF * $A.F - $nB * $A.B) div ($nA*$nA) ) else if ($nA * $nF + $A.F <= 0) then ( point:distance($P0, $P2) ) else if ($nA * $nB + $A.B <= 0) then ( point:distance($P0, $P2) ) else ( ( ($nF * $A.F - $nB * $A.B) div ($nA*$nA) ) + ( (($nB*$nB div $nA) - ($A.B*$A.B) div ($nA*$nA*$nA)) * (math:log($nA*$nF + $A.F) - math:log($nA*$nB + $A.B)) ) ) ) }; (:~ : cubic-length() : Travel length along a cubic edge. There is no general closed form, : so this is approximated by interpolating the edge with line segments. : : @param $cubic: the cubic edge : @param $n: number of interpolations to use :) declare function this:cubic-length( $cubic as map(xs:string,item()*), $n as xs:integer ) as xs:double { let $ts := util:linspace($n, 0, 1) let $pts := for $t in $ts return this:cubic-point($cubic, $t) return sum( for $pt at $i in tail($pts) return point:distance($pts[$i], $pt) ) }; (:~ : cubic-length() : Travel length along a cubic edge. There is no general closed form, : so this is approximated by interpolating the edge with 10 line segments. : : @param $cubic: the cubic edge :) declare function this:cubic-length($cubic as map(xs:string,item()*)) as xs:double { this:cubic-length($cubic, 10) }; (:~ : arc-length() : Travel distance along arc :) (: For 360 d=2πR; for α d=2πRα/360 i.e. the αth fraction of the total :) declare function this:arc-length($arc as map(xs:string,item()*)) as xs:double { let $r := $arc=>this:arc-radius() let $α := this:arc-extent($arc) return 2 * math:pi() * $r * $α div 360 }; (:~ : arc-extent() : Degrees swept out by an arc edge. : : @param $arc: the arc edge :) declare function this:arc-extent($arc as map(xs:string,item()*)) as xs:double { let $center := $arc=>this:arc-center() let $points := $arc=>this:arc-ends() let $angles := $arc=>this:arc-angles() return ( if (exists($points)) then ( util:remap-degrees(abs(this:angle($center, $points[2]) - this:angle($center, $points[1]))) ) else ( util:remap-degrees(abs($angles[2] - $angles[1])) ) ) }; (:~ : slice() : Create two edges at the given fraction of the source edge. Cubic edges : will be sliced into two cubic edges, quadratric edges will be slices into : two quadratic edges, etc. : : If t is at end, get same edge back. : : @param $edge: the edge to cut : @param $t: fraction of edge [0,1] :) declare function this:slice( $edge as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { switch (this:kind($edge)) case "edge" return this:slice-linear($edge, $t) case "arc" return this:slice-arc($edge, $t) case "quad" return this:slice-quad($edge, $t) case "cubic" return this:slice-cubic($edge, $t) default return errors:error("GEOM-BADREGION", ($edge, "slice")) }; (:~ : slice() : Slice the edge between two cut points. Will end up with three edges in : general: [start, p(start-t)], [p(start-t), p(end-t)], [p(end-t), end] : start-t <= end-t : : If start-t=end-t this is same as slice(edge, start-t) : If start-t or end-t is at an end will get one less edge : : Cubics are cut into cubics, quads into quads, straight edges into straight : edges : : @param $edge: the edge to cut : @param $start-t: fraction of edge [0,1] : @param $end-t: fraction of edge[0,1] :) declare function this:slice( $edge as map(xs:string,item()*), $start-t as xs:double, $end-t as xs:double ) as map(xs:string,item()*)* { if ($start-t > $end-t) then errors:error("GEOM-BADT", ($start-t, $end-t)) else (), if ($start-t = $end-t) then this:slice($edge, $start-t) else if ($start-t = 1) then $edge (: start-t=end-t=1 :) else if ($start-t = 0) then this:slice($edge, $end-t) else ( (: : end-t needs to be adjusted for the second slice: : Effective end-t should be fraction of length(slice) not of length(edge) : e * l - l1 = x * l2; l1 = s * l, l2 = l - l1 = l - s * l : x = (el - sl) / (s - sl) = (e - s) / (1 - s) :) let $slice := this:slice($edge, $start-t) let $eff-end-t := ($end-t - $start-t) div (1 - $start-t) return ($slice[1], this:slice($slice[2], $eff-end-t)) ) }; (:~ : chop() : Slice the edge by chopping it at the given cut points. : : Cubics are cut into cubics, quads into quads, straight edges into straight : edges : : @param $edge: the edge to cut : @param $ts: fraction of edge [0,1] :) declare function this:chop( $edge as map(xs:string,item()*), $ts as xs:double* ) as map(xs:string,item()*)* { let $ts := distinct-values(for $t in $ts order by $t ascending return $t) let $eff-ts := (: Adjust ts: : Need to adjust t2 : Effective t2 should be fraction of length(slice) : e * l - l1 = x * l2; l1 = s * l, l2 = l - l1 = l - s * l : x = (el - sl) / (s - sl) = (e - s) / (1 - s) : However, the t1 is itself an adjustment : So t4 needs to be adjusted to t3 which is adjusted to t2 : which is adjusted to t1 and so on. But we need to adjust : them all for each go round: : [ t1 t2 t3 ] : [ t1][ t2' t3'] : [ t1][ t2'][ t3''] :) fold-left(2 to count($ts) - 1, $ts, function ($new-ts as xs:double*, $it as xs:integer) as xs:double* { $new-ts[position() < $it - 1], fold-left($new-ts[position() >= $it], $new-ts[position() = $it - 1], function ($new-ts as xs:double*, $t2 as xs:double) as xs:double* { $new-ts, let $t1 := $new-ts[last()] return ($t2 - $t1) div (1 - $t1) } ) } ) let $split1 := this:slice($edge, head($eff-ts)) let $splits := ( fold-left(tail($eff-ts), array{$split1}, function ($splits as array(*)*, $t as xs:double) as array(*)* { $splits, array { if (empty($splits[last()]) or array:size($splits[last()]) < 2) then () else this:slice($splits[last()](2), $t) } } ) ) return ( for $split in $splits return $split?* ) }; (:~ : slice-linear() : Create two straight edges at the given fraction of the source edge. : : If t is at end, get same edge back. : : @param $this: the straight edge to cut : @param $t: fraction of edge [0,1] :) declare function this:slice-linear( $edge as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { if ($t = (0,1)) then $edge else let $p1 := this:start($edge) let $p2 := this:end($edge) let $pt := this:linear-point($edge, $t) return ( this:edge($p1, $pt), this:edge($pt, $p2) ) }; (:~ : slice-quad() : Create two quadratic edges at the given fraction of the source quadratic. : : If t is at end, get same edge back. : : @param $quad: the quad edge to cut : @param $t: fraction of edge [0,1] :) declare function this:slice-quad( $quad as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { if ($t = (0,1)) then $quad else let $p1 := this:start($quad) let $p2 := this:controls($quad)[1] let $p3 := this:end($quad) let $p12 := point:add($p1, point:sub($p2, $p1)=>point:times($t)) let $p23 := point:add($p2, point:sub($p3, $p2)=>point:times($t)) let $p123 := point:add($p12, point:sub($p23, $p12)=>point:times($t)) return ( (: Avoid creating quads that are really lines :) if (point:same($p1, $p2, $config:ε) or point:same($p2, $p3, $config:ε)) then this:slice-linear(this:edge($p1, $p3), $t) else ( if (point:same($p1, $p12, $config:ε) or point:same($p12, $p123, $config:ε)) then ( this:edge($p1, $p123) ) else ( this:quad($p1, $p123, $p12) ), if (point:same($p123, $p23, $config:ε) or point:same($p23, $p3, $config:ε)) then ( this:edge($p123, $p3) ) else ( this:quad($p123, $p3, $p23) ) ) ) }; (:~ : slice-cubic() : Create two cubic edges at the given fraction of the source cubic. : : If t is at end, get same edge back. : : @param $cubic: the cubic edge to cut : @param $t: fraction of edge [0,1] :) declare function this:slice-cubic( $cubic as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { if ($t = (0,1)) then $cubic else let $p1 := this:start($cubic) let $p2 := this:controls($cubic)[1] let $p3 := this:controls($cubic)[2] let $p4 := this:end($cubic) let $p12 := point:add($p1, point:sub($p2, $p1)=>point:times($t)) let $p23 := point:add($p2, point:sub($p3, $p2)=>point:times($t)) let $p34 := point:add($p3, point:sub($p4, $p3)=>point:times($t)) let $p123 := point:add($p12, point:sub($p23, $p12)=>point:times($t)) let $p234 := point:add($p23, point:sub($p34, $p23)=>point:times($t)) let $p1234 := point:add($p123, point:sub($p234, $p123)=>point:times($t)) return ( if (point:same($p1, $p2, $config:ε) or point:same($p2, $p3, $config:ε)) then this:slice-quad(this:quad($p1, $p3, $p4), $t) else if (point:same($p4, $p3, $config:ε)) then this:slice-quad(this:quad($p1, $p2, $p4), $t) else ( (: Avoid creating "cubics" that are really quads or lines :) if (point:same($p12, $p123, $config:ε)) then ( if (point:same($p1, $p12, $config:ε) or point:same($p123, $p1234, $config:ε)) then ( (: Really a line: make a straight edge :) this:edge($p1, $p1234) ) else ( (: Really a quad: make a quad edge :) this:quad($p1, $p1234, $p12) ) ) else ( this:cubic($p1, $p1234, $p12, $p123) ), if (point:same($p234, $p34, $config:ε)) then ( if (point:same($p1234, $p234, $config:ε) or point:same($p34, $p4, $config:ε)) then ( (: Really a line: make a straight edge :) this:edge($p1234, $p4) ) else ( (: Really a quad: make a quad edge :) this:quad($p1234, $p4, $p234) ) ) else ( this:cubic($p1234, $p4, $p234, $p34) ) ) ) }; (:~ : slice-arc() : Create two arc edges at the given fraction of the source arc. : If t is at end, get same edge back. : Normalizes the arc : : @param $arc: the arc edge to cut : @param $t: fraction of edge [0,1] :) declare function this:slice-arc( $arc as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*)* { if ($t = (0,1)) then $arc else let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $da := $to-angle - $from-angle let $at := $from-angle + $t*$da let $larges := if ($arc=>this:arc-large()) then ( util:remap-degrees($at - $from-angle) > 180, util:remap-degrees($to-angle - $at) > 180 ) else ( false(), false() ) let $a1 := this:arc-by-angle($center, $radius, util:remap-degrees($from-angle), util:remap-degrees($at), false(), $larges[1]) let $a2 := this:arc-by-angle($center, $radius, util:remap-degrees($at), util:remap-degrees($to-angle), false(), $larges[2]) return ( if (point:same(this:start($arc), this:end($arc), $config:ε) and not(this:arc-large($arc))) then ( this:slice-linear(this:edge(this:start($arc), this:end($arc)), $t) ) else ( if (not($larges[1]) and point:same(this:start($a1), this:end($a1), $config:ε)) then ( this:edge(this:start($a1), this:end($a1)) ) else ( $a1 ), if (not($larges[2]) and point:same(this:start($a2), this:end($a2), $config:ε)) then ( this:edge(this:start($a2), this:end($a2)) ) else ( $a2 ) ) ) }; (:~ : midpoint() : Return midpoint between two points : : @param $a: one point : @param $b: the other :) declare function this:midpoint($a as map(xs:string,item()*), $b as map(xs:string,item()*)) as map(xs:string,item()*) { point:midpoint($a, $b) }; (:~ : midpoint() : Center of the edge : Note: if you use on an arc, the arc must be normalized (edge:normalize-arc) :) declare function this:midpoint($edge as map(xs:string,item()*)) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return point:midpoint(this:start($edge), this:end($edge)) case "quad" return this:quad-point($edge, 0.5) case "cubic" return this:cubic-point($edge, 0.5) case "arc" return this:arc-point($edge, 0.5) default return errors:error("GEOM-BADREGION", ($edge, "midpoint")) }; (:~ : arc-midpoint() : Center of an arc. Note: arcs are treated as 2D, so output is 2D point; : we only look at x and y coordinates in the calculations. : : @param $center: center of the circle defining the arc : @param $a: start of arc : @param $p: end of arc :) declare function this:arc-midpoint( $center as map(xs:string,item()*), $a as map(xs:string,item()*), $b as map(xs:string,item()*) ) as map(xs:string,item()*) { let $r := point:distance($center, $a) let $lm := this:midpoint($a, $b) let $v := point:sub($lm, $center) let $lenv := math:sqrt(point:px($v)*point:px($v) + point:py($v)*point:py($v)) return if (point:sorientation($a, $b, $center) > 0) then ( point:sub( $center, point:point($r*point:px($v) div $lenv, $r*point:py($v) div $lenv) ) ) else ( point:add( $center, point:point($r*point:px($v) div $lenv, $r*point:py($v) div $lenv) ) ) }; (:~ : edge-point() : Compute the point on the edge the given fraction from the start : : @param $edge: the edge : @param $t: fraction of distance from start to end (t) [0,1] :) declare function this:edge-point($edge as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*) { switch (this:kind($edge)) case "edge" return this:linear-point($edge, $t) case "arc" return this:arc-point($edge, $t) case "quad" return this:quad-point($edge, $t) case "cubic" return this:cubic-point($edge, $t) default return errors:error("GEOM-BADREGION", ($edge, "edge-point")) }; (:~ : point-distance() : Shortest distance from point to linear edge :) declare function this:point-distance( $edge as map(xs:string,item()*), $pt as map(xs:string,item()*) ) as xs:double { if (point:same(this:start($edge), this:end($edge))) then ( point:distance($pt, this:start($edge)) ) else ( let $v := this:start($edge) let $w := this:end($edge) let $l := this:linear-length($edge) let $lsquared := $l*$l let $w_sub_v := point:sub($w, $v) let $t := max((0, min((1, point:dot(point:sub($pt, $v), $w_sub_v) div $lsquared)))) return ( point:distance($pt, point:point( point:px($v) + $t * point:px($w_sub_v), point:py($v) + $t * point:py($w_sub_v) ) ) ) ) }; (:~ : closest-point() : Point closest to the target point on the (linear) edge :) declare function this:closest-point( $edge as map(xs:string,item()*), $pt as map(xs:string,item()*) ) as map(xs:string,item()*) { let $start := this:start($edge) let $end := this:end($edge) return ( if (point:px($start)=point:px($end)) then point:point(point:px($start), point:py($pt)) else if (point:py($start)=point:py($end)) then point:point(point:px($pt), point:py($start)) else ( let $m1 := (point:py($end) - point:py($start)) div (point:px($end) - point:px($start)) let $m2 := -1 div $m1 let $x := ($m1*point:px($start) - $m2*point:px($pt) + point:py($pt) - point:py($start)) div ($m1 - $m2) let $y := $m2*($x - point:px($pt)) + point:py($pt) return ( point:point($x, $y) ) ) ) }; (:~ : on-segment-colinear() : Given edge and colinear point, check whether point is on that edge :) declare function this:on-segment-colinear( $edge as map(xs:string,item()*), $p as map(xs:string,item()*), $tolerance as xs:double ) as xs:boolean { util:twixt(point:px($p), min(this:vertices($edge)!point:px(.)) - $tolerance, max(this:vertices($edge)!point:px(.)) + $tolerance ) and util:twixt(point:py($p), min(this:vertices($edge)!point:py(.)) - $tolerance, max(this:vertices($edge)!point:py(.)) + $tolerance ) }; declare function this:on-segment-colinear( $edge as map(xs:string,item()*), $p as map(xs:string,item()*) ) as xs:boolean { this:on-segment-colinear($edge, $p, $config:tolerance) }; (:~ : on-segment() : Given edge and arbitary point, check whether point is on that edge :) declare function this:on-segment( $edge as map(xs:string,item()*), $p as map(xs:string,item()*) ) as xs:boolean { this:on-segment($edge, $p, $config:tolerance) }; declare function this:on-segment( $edge as map(xs:string,item()*), $p as map(xs:string,item()*), $tolerance as xs:double ) as xs:boolean { ( (point:sorientation(this:start($edge), this:end($edge), $p)=0) or (this:point-distance($edge, $p) < $tolerance) ) and ( util:twixt(point:px($p), min(this:vertices($edge)!point:px(.)) - $tolerance, max(this:vertices($edge)!point:px(.)) + $tolerance ) ) and ( util:twixt(point:py($p), min(this:vertices($edge)!point:py(.)) - $tolerance, max(this:vertices($edge)!point:py(.)) + $tolerance ) ) }; (:~ : linear-t() : Compute the t value of the point wrt the linear edge. :) declare function this:linear-t( $edge as map(xs:string,item()*), $point as map(xs:string,item()*) ) as xs:double* { if (this:on-segment-colinear($edge, $point)) then ( point:distance(this:start($edge), $point) div this:length($edge) ) else () }; (:~ : on-quad() : True if the point is on the quadractic edge. : : B(t)=(1-t)²P0 + 2(1-t)tP1 + t²P2, 0 <= t <= 1 : B(t)=(P0 - 2P1 + P2)t² + (2P1 - 2P0)t + P0 : If Q is on quad then solve (P0 - 2P1 + P2)t² + (2P1 - 2P0)t + P0 - Q = 0 : i.e. (-b ± √((b² - 4ac))/2a where a=(P0 - 2P1 + P2), b=(2P1 - 2P0), c = (P0 - Q) : If t in range then you're good : : We're using equality within ε here, because floating point equality is : problematic. Alternatively: you could compute minimum distance and : make sure it is within tolerance. :) declare function this:on-quad($quad as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { exists(this:quad-t($quad, $point, $tolerance)) }; declare function this:on-quad($quad as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean { this:on-quad($quad, $point, $config:tolerance) }; (:~ : quad-t() : Compute the t value of the point wrt the quadractic edge. : : B(t)=(1-t)²P0 + 2(1-t)tP1 + t²P2, 0 <= t <= 1 : B(t)=(P0 - 2P1 + P2)t² + (2P1 - 2P0)t + P0 : If Q is on quad then solve (P0 - 2P1 + P2)t² + (2P1 - 2P0)t + P0 - Q = 0 : i.e. (-b ± √((b² - 4ac))/2a where a=(P0 - 2P1 + P2), b=(2P1 - 2P0), c = (P0 - Q) : If t in range then you're good : : We're using equality within ε here, because floating point equality is : problematic. Alternatively: you could compute minimum distance and : make sure it is within tolerance. :) (: XYZZY TODO: how should tolerance be handled, really? :) declare function this:quad-t($quad as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:double* { if (point:same($point, this:start($quad), $tolerance)) then 0.0 else if (point:same($point, this:end($quad), $tolerance)) then 1.0 else ( let $P0 := this:start($quad) let $P1 := this:controls($quad)[1] let $P2 := this:end($quad) let $ax := point:px($P0) - 2*point:px($P1) + point:px($P2) let $bx := 2*point:px($P1) - 2*point:px($P0) let $cx := point:px($P0) - point:px($point) let $x-all-zero := every $x in ($ax, $bx, $cx) satisfies abs($x) < $config:ε let $ay := point:py($P0) - 2*point:py($P1) + point:py($P2) let $by := 2*point:py($P1) - 2*point:py($P0) let $cy := point:py($P0) - point:py($point) let $y-all-zero := every $y in ($ay, $by, $cy) satisfies abs($y) < $config:ε return ( if ($x-all-zero and $y-all-zero) then ( (: : Quad is really a point : If this works it was caught above :) ) else if ($x-all-zero) then ( (: : Quad is really a straight line with all x's the same : If this works point x must be same, then use ty :) if (abs(point:px($point) - point:px($P0)) < $tolerance) then ( let $ty := roots:quadratic-real-roots($ay, $by, $cy)[util:twixt(., -$config:ε, 1 + $config:ε)] for $t in $ty return util:clamp($t, 0.0, 1.0) ) else ( ) ) else if ($y-all-zero) then ( (: : Quad is really a straight line with all y's the same : If this works point y must be same, then use tx :) if (abs(point:py($point) - point:py($P0)) < $tolerance) then ( let $tx := roots:quadratic-real-roots($ax, $bx, $cx)[util:twixt(., -$config:ε, 1 + $config:ε)] for $t in $tx return util:clamp($t, 0.0, 1.0) ) else ( ) ) else ( (: : Normal case: match tx with ty :) let $tx := roots:quadratic-real-roots($ax, $bx, $cx)[util:twixt(., -$config:ε, 1 + $config:ε)] let $ty := roots:quadratic-real-roots($ay, $by, $cy)[util:twixt(., -$config:ε, 1 + $config:ε)] let $ts := $tx[some $t in $ty satisfies abs(. - $t) < $config:ε] for $t in $ts return util:clamp($t, 0.0, 1.0) ) ) ) }; declare function this:quad-t($quad as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double* { this:quad-t($quad, $point, $config:tolerance) }; (:~ : on-cubic() : True if the point is on the cubic edge. : B(t)=(1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3, 0 <= t <= 1 : B(t)=(P3 - 3P2 + P1 - P0)t³ + (3P2 - 6P1)t² + (3P1)t + P0 : If Q is on cubic then solve : (P3 - 3P2 + P1 - P0)t³ + (3P2 - 6P1)t² + (3P1)t + P0 - Q = 0 : i.e. cubic roots : : If t in range then you're good : : We're using equality within ε here, because floating point equality is : problematic. Alternatively: you could compute minimum distance and : make sure it is within tolerance. :) declare function this:on-cubic($cubic as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { exists(this:cubic-t($cubic, $point, $tolerance)) }; declare function this:on-cubic($cubic as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean { this:on-cubic($cubic, $point, $config:tolerance) }; (:~ : cubic-t() : Compute the t value of the point wrt the cubic edge. : B(t)=(1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3, 0 <= t <= 1 : B(t)=(-P0 + 3P1 - 3P2 + P3)t³ + (3P0 - 6P1 + 3P2)t² + (-3P0 + 3P1)t + P0 : If Q is on cubic then solve : (-P0 + 3P1 - 3P2 + P3)t³ + (3P0 - 6P1 + 3P2)t² + (-3P0 + 3P1)t + (P0 - Q) : i.e. cubic roots : : If t in range then you're good : : We're using equality within ε here, because floating point equality is : problematic. Alternatively: you could compute minimum distance and : make sure it is within tolerance. :) (: XYZZY TODO: how should tolerance be handled, really? :) declare function this:cubic-t($cubic as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:double* { if (point:same($point, this:start($cubic), $tolerance)) then 0.0 else if (point:same($point, this:end($cubic), $tolerance)) then 1.0 else ( let $P0 := this:start($cubic) let $P1 := this:controls($cubic)[1] let $P2 := this:controls($cubic)[2] let $P3 := this:end($cubic) let $ax := -point:px($P0) + 3*point:px($P1) - 3*point:px($P2) + point:px($P3) let $bx := 3*point:px($P0) - 6*point:px($P1) + 3*point:px($P2) let $cx := -3*point:px($P0) + 3*point:px($P1) let $dx := point:px($P0) - point:px($point) let $x-all-zero := every $x in ($ax, $bx, $cx, $dx) satisfies abs($x) < $config:ε let $ay := -point:py($P0) + 3*point:py($P1) - 3*point:py($P2) + point:py($P3) let $by := 3*point:py($P0) - 6*point:py($P1) + 3*point:py($P2) let $cy := -3*point:py($P0) + 3*point:py($P1) let $dy := point:py($P0) - point:py($point) let $y-all-zero := every $y in ($ay, $by, $cy, $dy) satisfies abs($y) < $config:ε return ( if ($x-all-zero and $y-all-zero) then ( (: : Cubic is really a point : If this works it was caught above :) ) else if ($x-all-zero) then ( (: : Cubic is really a straight line with all x's the same : If this works point x must be same, then use ty :) if (abs(point:px($point) - point:px($P0)) < $tolerance) then ( let $ty := roots:cubic-real-roots($ay, $by, $cy, $dy)[util:twixt(., -$config:ε, 1 + $config:ε)] for $t in $ty return util:clamp($t, 0.0, 1.0) ) else ( ) ) else if ($y-all-zero) then ( (: : Cubic is really a straight line with all y's the same : If this works point y must be same, then use tx :) if (abs(point:py($point) - point:py($P0)) < $tolerance) then ( let $tx := roots:cubic-real-roots($ax, $bx, $cx, $dx)[util:twixt(., -$config:ε, 1 + $config:ε)] for $t in $tx return util:clamp($t, 0.0, 1.0) ) else ( ) ) else ( (: : Normal case: match tx with ty :) let $tx := roots:cubic-real-roots($ax, $bx, $cx, $dx)[util:twixt(., -$config:ε, 1 + $config:ε)] let $ty := roots:cubic-real-roots($ay, $by, $cy, $dy)[util:twixt(., -$config:ε, 1 + $config:ε)] let $ts := $tx[some $t in $ty satisfies abs(. - $t) < $config:ε] for $t in $ts return util:clamp($t, 0.0, 1.0) ) ) ) }; declare function this:cubic-t($cubic as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double* { this:cubic-t($cubic, $point, $config:tolerance) }; (:~ : on-arc() : Is the point on the arc (within ε)? : Note: will normalize the arc :) declare function this:on-arc( $arc as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double ) as xs:boolean { let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] return ( abs(point:distance($center, $point) - $radius) < $tolerance and ( util:twixt(this:angle($center, $point), $from-angle, $to-angle) or util:twixt(this:angle($center, $point)+360, $from-angle, $to-angle) ) ) }; declare function this:on-arc( $arc as map(xs:string,item()*), $point as map(xs:string,item()*) ) as xs:boolean { this:on-arc($arc, $point, $config:tolerance) }; (:~ : normalize-arc() : A lot of arc operations need us to know the actual arc center and : starting/ending angles. For large and flipped arcs that's get : problematic. Rather than repeat that code everywhere, encapsulate : it here. The normalized arc will always run in sweep order and may or : may not be large. : : @param $arc: source arc :) declare function this:normalize-arc( $arc as map(xs:string,item()*) ) as map(xs:string,item()*) { let $arc := this:as-angle-arc($arc) let $flipped := $arc=>this:arc-flipped() let $large := $arc=>this:arc-large() let $angles := ($arc=>this:arc-angles())!util:remap-degrees(.) let $delta := $angles[2] - $angles[1] let $swap := ($large and ($delta < -180 or ($delta > 0 and $delta < 180)) ) or (not($large) and (($delta < 0 and abs($delta) < 180) or ($delta > 180)) ) let $adjust-from := ($large and $delta > 0 and $delta < 180) or (not($large) and $delta > 180) let $adjust-to := ($large and $delta < 0 and abs($delta) <= 180) or (not($large) and $delta <= -180) let $adjusted-from := if ($adjust-from) then $angles[1] + 360 else $angles[1] let $adjusted-to := if ($adjust-to) then $angles[2] + 360 else $angles[2] let $from-angle := if ($swap) then $adjusted-to else $adjusted-from let $to-angle := if ($swap) then $adjusted-from else $adjusted-to let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $from := point:destination($center, $from-angle, $radius) let $to := point:destination($center, $to-angle, $radius) let $flipped-center := if ($swap) then ( if ($flipped) then $center else affine:reflect($center, $from, $to) ) else ( if ($flipped) then affine:reflect($center, $from, $to) else $center ) return ( util:assert($from-angle <= $to-angle, "Normalized angles wonky"), util:assert((abs($to-angle - $from-angle) > 180) = $large, "Large flag wonky"), this:arc-by-angle( $flipped-center, $radius, $from-angle, $to-angle, false(), $large ) ) }; (:~ : arc-t() : Compute the t value of the point wrt the arc edge. : Will normalize the arc :) declare function this:arc-t( $arc as map(xs:string,item()*), $point as map(xs:string,item()*) ) as xs:double* { if (point:same($point, this:start($arc))) then 0.0 else if (point:same($point, this:end($arc))) then 1.0 else ( let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $point-angle := this:angle($center, $point) return ( if (abs(point:distance($center, $point) - $radius) < $config:ε and ( util:twixt($point-angle, $from-angle, $to-angle) or util:twixt($point-angle + 360, $from-angle, $to-angle) ) ) then ( ($point-angle - $from-angle) div ($to-angle - $from-angle) ) else () ) ) }; declare function this:edge-t( $edge as map(xs:string,item()*), $point as map(xs:string,item()*) ) as xs:double* { switch (this:kind($edge)) case "quad" return this:quad-t($edge, $point) case "cubic" return this:cubic-t($edge, $point) case "edge" return this:linear-t($edge, $point) case "arc" return this:arc-t($edge, $point) default return errors:error("GEOM-NOTIMPLEMENTED", ("edge-t", this:kind($edge))) }; (:====================================================================== : Operations :======================================================================:) (:~ : bounding-box() : Minimum box surrounding the region. Some approximation for non-linear : edges. :) declare function this:bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*) { let $boxes := ( for $region in $regions return switch (this:kind($region)) case "edge" return let $pts := this:points($region) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) case "quad" return ( (: P0 = start, P1 = control, P2 = end :) (: B(t) = (1-t)²·P0 + 2(1-t)t·P1 + t²·P2 :) (: B'(t) = a(1-t) + b where a = 2(B-A), b=2(C-B) :) (: A=P0 B=P1 C=P2 :) (: B'(t) = 0 where t = -a/(b-a) :) let $P0 := this:start($region) let $P1 := this:controls($region)[1] let $P2 := this:end($region) let $T := point:map3( function ($p0 as xs:double, $p1 as xs:double, $p2 as xs:double) as xs:double { let $A := $p0 let $B := $p1 let $C := $p2 let $a := 2*($B - $A) let $b := 2*($C - $B) return if ($a = $b) then xs:double("INF") else -$a div ($b - $a) }, $P0, $P1, $P2 ) let $pts := ( $P0, $P2, if (util:twixt(point:px($T), 0, 1)) then $region=>this:quad-point(point:px($T)) else (), if (util:twixt(point:py($T), 0, 1)) then $region=>this:quad-point(point:py($T)) else () ) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) ) case "cubic" return ( (: Cubic P1 to P4 with controls P2, P3 :) (: B(t) = (1-t)³·P1 + 3(1-t)²t·P2 + 3(1-t)t²·P3 + t³·P4 :) (: B'(t) = (1-t)²·A + 2(1-t)t·B + t²·C :) (: A=3(P2-P1), B=3(P3-P2), C=3(P4-P3) :) (: B'(t) = at² + bt + c = 0 => t = (-b ± √(b² - 4ac))/2a :) (: a = A - 2B + C = 3(-P1 + 3P2 - 3P3 + P4) :) (: b = 2(B - A) = 6(P1 - 2P2 + P3) :) (: c = A = 3(P2 - P1) :) let $P1 := this:start($region) let $P2 := this:controls($region)[1] let $P3 := this:controls($region)[2] let $P4 := this:end($region) let $ts := ( let $p1 := point:px($P1) let $p2 := point:px($P2) let $p3 := point:px($P3) let $p4 := point:px($P4) let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) let $c := 3*($p2 - $p1) return roots:quadratic-real-roots($a, $b, $c), let $p1 := point:py($P1) let $p2 := point:py($P2) let $p3 := point:py($P3) let $p4 := point:py($P4) let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) let $c := 3*($p2 - $p1) return roots:quadratic-real-roots($a, $b, $c) ) let $pts := ( $P1, $P4, for $t in $ts return ( if (util:twixt($t, 0, 1)) then $region=>this:cubic-point($t) else () ) ) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) ) case "arc" return ( let $arc := this:normalize-arc($region) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $from := $arc=>this:start() let $to := $arc=>this:end() (: If the arc is less than 180° we can do better than the whole : circle's bounding box by computing the intersection of the : tangent lines and finding the point from that to center at r : Tangent line to p has slope angle(center,p)+90 : Equation y = mx + b => p[y] = mp[x] + b => b = p[y] - mp[x] : Intersection of a1x + b1y + c1 = 0, a2x + b2y + c2 = 0 : is ((b1c2 - b2c1)/(a1b2 - a2b1), (c1a2 - c2a1)/(a1b2 - a2b1)) : a1=m b1=-1 c1=p[y]-mp[x] : a2=n b2=-1 c2=q[y]-mq[x] :) return ( if ($from-angle = $to-angle) then ( box:box($from, $from) ) else if ($to-angle - $from-angle < 180) then ( let $m := util:radians($from-angle - 90) let $a1 := $m let $b1 := -1 let $c1 := point:py($from) - $m*point:px($from) let $n := util:radians($to-angle - 90) let $a2 := $n let $b2 := -1 let $c2 := point:py($to) - $n*point:px($to) let $intersection := ( util:assert($a1*$b2 - $a2*$b1 != 0, "Non intersecting lines"), point:point( ($b1*$c2 - $b2*$c1) div ($a1*$b2 - $a2*$b1), ($c1*$a2 - $c2*$a1) div ($a1*$b2 - $a2*$b1) ) ) let $pts := ( $from, $to, point:destination($center, this:angle($center, $intersection), $radius) ) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) ) else ( ellipse:bounding-box(this:arc-circle($arc)) ) ) ) default return errors:error("GEOM-BADREGION", ($region, "bounding-box")) ) return ( if (empty($boxes)) then box:box(0,0,0,0) else if (empty(tail($boxes))) then $boxes else ( let $pts := $boxes!this:points(.) return box:box( min($pts!point:px(.)), min($pts!point:py(.)), max($pts!point:px(.)), max($pts!point:py(.)) ) ) ) }; (:====================================================================== : Intersections :======================================================================:) (:~ : line-intersection() : Point of intersection, if any between lines defined as extension of : two edges. : Assumes linear edges: so wrong for other kinds of edges :) declare function this:line-intersection( $this as map(xs:string,item()*), $that as map(xs:string,item()*) ) as map(xs:string,item()*)? { (: edge: ((x1,y1), (x2,y2)) => line Ax + By = C : A = y2 - y1 : B = x1 - x2 : C = Ax1 + By1 : det = A1 * B2 - A2 * B1 : => ( (B2 * C1 - B1 * C2) / det, (A1 * C2 - A2 * C1) / det :) (: A = y2 - y1 ; Ax = A(this), Ay = A(that) :) let $A := ( point:py(this:end($this)) - point:py(this:start($this)), point:py(this:end($that)) - point:py(this:start($that)) ) (: B = x1 - x2 ; Bx = B(this), By = B(that) :) let $B := ( point:px(this:start($this)) - point:px(this:end($this)), point:px(this:start($that)) - point:px(this:end($that)) ) (: C = Ax1 + By1 ; Cx = C(this), Cy = C(that) :) let $C := ( v:px($A) * point:px(this:start($this)) + v:px($B) * point:py(this:start($this)), v:py($A) * point:px(this:start($that)) + v:py($B) * point:py(this:start($that)) ) let $det := v:determinant($A, $B) return ( if ($det = (0,-0)) then () else ( point:point( -v:determinant($B, $C) div $det, v:determinant($A, $C) div $det ) ) ) }; (:~ : intersection() : Point of intersection, if any between two edges :) declare function this:intersection( $this as map(xs:string,item()*), $that as map(xs:string,item()*) ) as map(xs:string,item()*)* { switch (this:kind($this)) case "edge" return this:linear-intersection($this, $that) case "quad" return this:quad-intersection($this, $that) case "cubic" return this:cubic-intersection($this, $that) case "arc" return this:arc-intersection($this, $that) default return errors:error("GEOM-BADREGION", ($this, "intersection")) }; (:~ : linear-intersection() : Point of intersection, if any between linear edge and some other edge :) declare function this:linear-intersection( $edge as map(xs:string,item()*), $other as map(xs:string,item()*) ) as map(xs:string,item()*)* { if (this:kind($edge) != "edge") then errors:error("GEOM-BADREGION", ($edge, "linear-intersection")) else ( switch(this:kind($other)) case "edge" return ( let $p := this:line-intersection($edge, $other) return ( if (empty($p)) then () else if (this:on-segment-colinear($edge, $p) and this:on-segment-colinear($other, $p)) then $p else () ) ) case "quad" return this:quad-intersection($other, $edge) case "cubic" return this:cubic-intersection($other, $edge) case "arc" return this:arc-intersection($other, $edge) default return errors:error("GEOM-BADREGION", ($edge, "linear-intersection")) ) }; declare function this:quad-edge-intersection-ts( $quad as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double ) as xs:double* { if (this:kind($quad) != "quad") then errors:error("GEOM-BADREGION", ($quad, "quad-edge-intersection-ts")) else if (this:kind($other) != "edge") then errors:error("GEOM-BADREGION", ($quad, "quad-edge-intersection-ts")) else ( (: https://pomax.github.io/bezierinfo/ :) let $angle := this:angle($other) let $rotated-edge := this:mutate($other, function ($p as map(xs:string,item()*)) as map(xs:string,item()*) {affine:rotate($p, $angle, this:start($other))} ) let $translation := point:py(this:start($rotated-edge)) let $translated-edge := this:mutate($rotated-edge, function ($p as map(xs:string,item()*)) as map(xs:string,item()*) {affine:translate($p, 0, -$translation)} ) let $translated-quad := this:mutate($quad, function ($p as map(xs:string,item()*)) as map(xs:string,item()*) {affine:rotate($p, $angle, this:start($other))=>affine:translate(0,-$translation)} ) let $P0 := this:start($translated-quad) let $P1 := this:controls($translated-quad)[1] let $P2 := this:end($translated-quad) let $ts := ( let $p0 := point:px($P0) let $p1 := point:px($P1) let $p2 := point:px($P2) let $a := $p0 - 2*$p1 + $p2 let $b := 2*$p1 - 2*$p0 let $c := $p0 return roots:quadratic-real-roots($a, $b, $c) , let $p0 := point:py($P0) let $p1 := point:py($P1) let $p2 := point:py($P2) let $a := $p0 - 2*$p1 + $p2 let $b := 2*$p1 - 2*$p0 let $c := $p0 return roots:quadratic-real-roots($a, $b, $c) ) return ( distinct-values( for $t in $ts where ( util:twixt($t, 0, 1) and ( let $qp := this:quad-point($translated-quad, $t) return abs(point:py($qp)) < $tolerance and util:twixt(point:px($qp), point:px(this:start($translated-edge)), point:px(this:end($translated-edge))) ) ) return $t ) ) ) }; declare function this:quad-edge-intersection-ts( $quad as map(xs:string,item()*), $other as map(xs:string,item()*) ) as xs:double* { this:quad-edge-intersection-ts($quad, $other, $config:tolerance) }; (:~ : quad-intersection() : Point of intersection, if any between quad edge and some other edge :) declare function this:quad-intersection( $quad as map(xs:string,item()*), $other as map(xs:string,item()*) ) as map(xs:string,item()*)* { if (this:kind($quad) != "quad") then errors:error("GEOM-BADREGION", ($quad, "quad-intersection")) else ( switch(this:kind($other)) case "edge" return ( let $good-ts := this:quad-edge-intersection-ts($quad, $other) for $t in $good-ts return this:quad-point($quad, $t) ) case "quad" return ( if (box:intersects-box(this:bounding-box($quad), this:bounding-box($other))) then ( if (this:quad-length($quad) < $config:ε and this:quad-length($other) < $config:ε) then ( this:midpoint($quad) ) else ( let $pts := let $quadsubs := this:slice-quad($quad, 0.5) let $othersubs := this:slice-quad($other, 0.5) for $subquad in $quadsubs for $subother in $othersubs return this:intersection($subquad, $subother) let $dpts := for $p in $pts return point:decimal($p, $this:precision) for $i in 1 to count($dpts) where every $j in 1 to $i - 1 satisfies ( not(point:same($dpts[$i], $dpts[$j])) ) return $pts[$i] ) ) else ( ) ) case "cubic" return this:cubic-intersection($other, $quad) default return errors:error("GEOM-BADREGION", ($other, "quad-intersection")) ) }; (:~ : cubic-intersection() : Point of intersection, if any between cubic edge and some other edge :) declare function this:cubic-intersection( $cubic as map(xs:string,item()*), $other as map(xs:string,item()*) ) as map(xs:string,item()*)* { if (this:kind($cubic) != "cubic") then errors:error("GEOM-BADREGION", ($cubic, "cubic-intersection")) else ( switch(this:kind($other)) case "edge" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other))) then ( if (this:linear-length($other) < $config:ε and this:cubic-length($cubic, 4) < $config:ε) then this:midpoint($cubic) else ( let $pts := let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-linear($other, 0.5) for $subcubic in $cubicsubs for $subother in $othersubs return this:intersection($subcubic, $subother) let $dpts := for $p in $pts return point:decimal($p, $this:precision) for $i in 1 to count($dpts) where every $j in 1 to $i - 1 satisfies ( not(point:same($dpts[$i], $dpts[$j])) ) return $pts[$i] ) ) else ( ) ) case "quad" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other))) then ( if (this:quad-length($other) < $config:ε and this:cubic-length($cubic, 4) < $config:ε) then this:midpoint($cubic) else ( let $pts := let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-quad($other, 0.5) for $subcubic in $cubicsubs for $subother in $othersubs return this:intersection($subcubic, $subother) let $dpts := for $p in $pts return point:decimal($p, $this:precision) for $i in 1 to count($dpts) where every $j in 1 to $i - 1 satisfies ( not(point:same($dpts[$i], $dpts[$j])) ) return $pts[$i] ) ) else ( ) ) case "cubic" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other))) then ( if (this:cubic-length($cubic, 4) < $config:ε and this:cubic-length($other, 4) < $config:ε) then this:midpoint($cubic) else ( let $pts := let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-cubic($other, 0.5) for $subcubic in $cubicsubs for $subother in $othersubs return this:intersection($subcubic, $subother) let $dpts := for $p in $pts return point:decimal($p, $this:precision) for $i in 1 to count($dpts) where every $j in 1 to $i - 1 satisfies ( not(point:same($dpts[$i], $dpts[$j])) ) return $pts[$i] ) ) else ( ) ) default return errors:error("GEOM-BADREGION", ($other, "cubic-intersection")) ) }; declare function this:arc-intersection( $arc as map(xs:string,item()*), $other as map(xs:string,item()*) ) as map(xs:string,item()*)* { if (this:kind($arc) != "arc") then errors:error("GEOM-BADREGION", ($arc, "arc-intersection")) else ( switch(this:kind($other)) case "edge" return ( let $arc := this:normalize-arc($arc) (: https://math.stackexchange.com/questions/228841/how-do-i-calculate-the-intersections-of-a-straight-line-and-a-circle :) (: line y=mx + c; m = (y2-y1)/(x2-x1), c=y1 :) let $r := this:arc-radius($arc) let $p := point:px(this:arc-center($arc)) let $q := point:py(this:arc-center($arc)) let $x1 := point:px(this:start($other)) let $y1 := point:py(this:start($other)) let $x2 := point:px(this:end($other)) let $y2 := point:py(this:end($other)) let $pts := ( if (abs($x1 - $x2) < $config:ε) then ( (: vertical line :) let $k := $x1 let $A := 1 let $B := -2*$q let $C := $p*$p + $q*$q - $r*$r - 2*$k*$p + $k*$k where $B*$B >= 4*$A*$C return ( for $y in roots:quadratic-real-roots($A, $B, $C) return point:point($k, $y) ) ) else ( let $m := ($y2 - $y1) div ($x2 - $x1) let $c := $y1 - $m*$x1 let $A := $m*$m + 1 let $B := 2*($m*$c - $m*$q - $p) let $C := $q*$q - $r*$r + $p*$p - 2*$c*$q + $c*$c where $B*$B >= 4*$A*$C return ( for $x in roots:quadratic-real-roots($A, $B, $C) return point:point($x, $m*$x + $c) ) ) ) return ( for $pt in $pts where this:on-segment-colinear($other, $pt) and this:on-arc($arc, $pt) return $pt ) ) case "quad" return ( errors:error("GEOM-NOTIMPLEMENTED", ("arc-intersection", this:kind($other))) ) case "cubic" return ( errors:error("GEOM-NOTIMPLEMENTED", ("arc-intersection", this:kind($other))) ) case "arc" return ( errors:error("GEOM-NOTIMPLEMENTED", ("arc-intersection", this:kind($other))) ) default return errors:error("GEOM-BADREGION", ($other, "arc-intersection")) ) }; (:~ linear edge to linear edge intersection :) declare function this:edge-intersects-edge( $edge as map(xs:string,item()*), $other as map(xs:string,item()*) ) as xs:boolean { this:edge-intersects-edge($edge, $other, $config:tolerance) }; declare function this:edge-intersects-edge( $edge as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double ) as xs:boolean { (: See https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/ :) let $o1 := point:sorientation(this:start($edge), this:end($edge), this:start($other)) let $o2 := point:sorientation(this:start($edge), this:end($edge), this:end($other)) let $o3 := point:sorientation(this:start($other), this:end($other), this:start($edge)) let $o4 := point:sorientation(this:start($other), this:end($other), this:end($edge)) return ( ($o1 != $o2 and $o3 != $o4) or (: $other.start colinear with and on segment $edge :) ($o1=0 and this:on-segment-colinear($edge, this:start($other), $tolerance)) or (: $other.end colinear with and on segment $edge :) ($o2=0 and this:on-segment-colinear($edge, this:end($other), $tolerance)) or (: $edge.start colinear with and on segment $other :) ($o3=0 and this:on-segment-colinear($other, this:start($edge), $tolerance)) or (: $edge.end colinear with and on segment $other :) ($o4=0 and this:on-segment-colinear($other, this:end($edge), $tolerance)) ) }; (:~ : linear-intersects() : Edge (straight) intersects some other region kind :) declare function this:linear-intersects($edge as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { switch (this:kind($other)) case "point" return this:on-segment($edge, $other, $tolerance) case "edge" return this:edge-intersects-edge($edge, $other, $tolerance) case "quad" return this:quad-intersects($other, $edge, $tolerance) case "cubic" return this:cubic-intersects($other, $edge, $tolerance) case "arc" return this:arc-intersects($other, $edge, $tolerance) default return errors:error("GEOM-BADREGION", ($other, "linear-intersects")) }; declare function this:linear-intersects($edge as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:linear-intersects($edge, $other, $config:tolerance) }; (:~ : quad-intersects() : Quad edge intersects some other region kind :) declare function this:quad-intersects($quad as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { switch (this:kind($other)) case "point" return this:on-quad($quad, $other, $tolerance) case "edge" return exists(this:quad-edge-intersection-ts($quad, $other, $tolerance)) case "quad" return ( if (box:intersects-box(this:bounding-box($quad), this:bounding-box($other), $tolerance)) then ( if (this:quad-length($quad) < $config:ε and this:quad-length($other) < $config:ε) then true() else ( let $quadsubs := this:slice-quad($quad, 0.5) let $othersubs := this:slice-quad($other, 0.5) return ( some $subquad in $quadsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subquad, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "cubic" return this:cubic-intersects($other, $quad, $tolerance) case "arc" return this:arc-intersects($other, $quad, $tolerance) default return errors:error("GEOM-BADREGION", ($other, "quad-intersects")) }; declare function this:quad-intersects($quad as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:quad-intersects($quad, $other, $config:tolerance) }; (:~ : cubic-intersects() : Cubic edge intersects some other region kind :) declare function this:cubic-intersects($cubic as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { switch (this:kind($other)) case "point" return ( this:on-cubic($cubic, $other, $tolerance) ) case "edge" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other), $tolerance)) then ( if (this:linear-length($other) < $tolerance and this:cubic-length($cubic, 4) < $tolerance) then true() else ( let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-linear($other, 0.5) return ( some $subcubic in $cubicsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subcubic, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "quad" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other), $tolerance)) then ( if (this:quad-length($other) < $tolerance and this:cubic-length($cubic, 4) < $tolerance) then true() else ( let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-quad($other, 0.5) return ( some $subcubic in $cubicsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subcubic, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "cubic" return ( if (box:intersects-box(this:bounding-box($cubic), this:bounding-box($other), $tolerance)) then ( if (this:cubic-length($cubic, 4) < $tolerance and this:cubic-length($other, 4) < $tolerance) then true() else ( let $cubicsubs := this:slice-cubic($cubic, 0.5) let $othersubs := this:slice-cubic($other, 0.5) return ( some $subcubic in $cubicsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subcubic, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "arc" return this:arc-intersects($other, $cubic, $tolerance) default return errors:error("GEOM-BADREGION", ($other, "cubic-intersects")) }; declare function this:cubic-intersects($cubic as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:cubic-intersects($cubic, $other, $config:tolerance) }; (:~ : arc-intersects() : Arc edge intersects some other region kind :) declare function this:arc-intersects($arc as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { if (point:same(this:start($arc), this:end($arc), $tolerance)) then ( if (this:arc-large($arc)) then ( this:edge-intersects(this:arc-circle(this:normalize-arc($arc)), $other, $tolerance) ) else ( this:edge-intersects(this:start($arc), $other, $tolerance) ) ) else switch (this:kind($other)) case "point" return this:on-arc($arc, $other, $tolerance) case "edge" return let $arc := this:normalize-arc($arc) (: https://math.stackexchange.com/questions/228841/how-do-i-calculate-the-intersections-of-a-straight-line-and-a-circle :) (: line y=mx + x; m = (y2-y1)/(x2-x1), c=y1 :) let $r := this:arc-radius($arc) let $p := point:px(this:arc-center($arc)) let $q := point:py(this:arc-center($arc)) let $x1 := point:px(this:start($other)) let $y1 := point:py(this:start($other)) let $x2 := point:px(this:end($other)) let $y2 := point:py(this:end($other)) let $pts := if (abs($x1 - $x2) < $config:ε) then ( (: vertical line :) let $k := $x1 let $A := 1 let $B := -2*$q let $C := $p*$p + $q*$q - $r*$r - 2*$k*$p + $k*$k where $B*$B >= 4*$A*$C return ( for $y in roots:quadratic-real-roots($A, $B, $C) return point:point($k, $y) ) ) else ( let $m := ($y2 - $y1) div ($x2 - $x1) let $c := $y1 - $m*$x1 let $A := $m*$m + 1 let $B := 2*($m*$c - $m*$q - $p) let $C := $q*$q - $r*$r + $p*$p - 2*$c*$q + $c*$c where $B*$B >= 4*$A*$C return ( for $x in roots:quadratic-real-roots($A, $B, $C) return point:point($x, $m*$x + $c) ) ) return ( some $pt in $pts satisfies this:on-segment-colinear($other, $pt, $tolerance) and this:on-arc($arc, $pt, $tolerance) ) case "quad" return ( if (box:intersects-box(this:bounding-box($arc), this:bounding-box($other), $tolerance)) then ( if (this:arc-length($arc) < $tolerance and this:quad-length($other) < $tolerance) then ( true() ) else ( let $arcsubs := this:slice-arc($arc, 0.5) let $othersubs := this:slice-quad($other, 0.5) return ( some $subarc in $arcsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subarc, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "cubic" return ( if (box:intersects-box(this:bounding-box($arc), this:bounding-box($other), $tolerance)) then ( if (this:arc-length($arc) < $tolerance and this:cubic-length($other, 4) < $tolerance) then true() else ( let $arcsubs := this:slice-arc($arc, 0.5) let $othersubs := this:slice-cubic($other, 0.5) return ( some $subarc in $arcsubs satisfies ( some $subother in $othersubs satisfies this:edge-intersects($subarc, $subother, $tolerance) ) ) ) ) else ( false() ) ) case "arc" return let $arc := this:normalize-arc($arc) return ( ellipse:ellipse-intersects-ellipse(this:arc-circle($arc), this:arc-circle($other), $tolerance) and ( (: https://stackoverflow.com/questions/3349125/circle-circle-intersection-points :) let $d := point:distance(this:arc-center($arc), this:arc-center($other)) return if ($d <= $tolerance) then ( some $pt in (this:start($other), this:end($other)) satisfies this:on-arc($arc, $pt, $tolerance) ) else ( let $r1 := this:arc-radius($arc) let $p1 := this:arc-center($arc) let $x1 := point:px($p1) let $y1 := point:py($p1) let $r2 := this:arc-radius($other) let $p2 := this:arc-center($other) let $x2 := point:px($p2) let $y2 := point:py($p2) let $l := ($r1*$r1 - $r2*$r2 + $d*$d) div (2*$d) let $h := math:sqrt($r1*$r1 - $l*$l) let $dp := $p2=>point:sub($p1) let $dx := $x2 - $x1 let $dy := $y2 - $y1 let $mid := $p1=>point:add($dp=>point:times($l div $d)) let $pts := ( $mid=>point:add($dp=>point:times($h div $d)), $mid=>point:sub($dp=>point:times($h div $d)) ) return ( some $pt in $pts satisfies this:on-arc($arc, $pt, $tolerance) and this:on-arc($other, $pt, $tolerance) ) ) ) ) default return errors:error("GEOM-BADREGION", ($other, "arc-intersects")) }; declare function this:arc-intersects($arc as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:arc-intersects($arc, $other, $config:tolerance) }; declare function this:edge-intersects($region as map(xs:string,item()*), $other as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean { switch (this:kind($region)) case "point" return this:edge-intersects($other, $region, $tolerance) case "edge" return this:linear-intersects($region, $other, $tolerance) case "quad" return this:quad-intersects($region, $other, $tolerance) case "cubic" return this:cubic-intersects($region, $other, $tolerance) case "arc" return this:arc-intersects($region, $other, $tolerance) default return errors:error("GEOM-BADREGION", ($other, "edge-intersects")) }; declare function this:edge-intersects($region as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean { this:edge-intersects($region, $other, $config:tolerance) }; (:~ : tangent() : Return the tangent vector to the given point on the edge (2D) :) declare function this:tangent( $region as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*) { switch(this:kind($region)) case "edge" return ( this:end($region)=>point:sub(this:start($region))=>point:normalize() ) case "quad" return ( (: : Derivative: : P0 = start, P1 = control, P2 = end : B(t) = (1-t)²·P0 + 2(1-t)t·P1 + t²·P2 : B'(t) = (1-t)·A + t·B : B'(t) = a(1-t) + b where a = 2(B-A), b=2(C-A) : A=P0 B=P1 :) let $P0 := this:start($region) let $P1 := this:controls($region)[1] let $P2 := this:end($region) return ( point:map3( function ($p0 as xs:double, $p1 as xs:double, $p2 as xs:double) as xs:double { let $A := $p0 let $B := $p1 let $C := $p2 let $a := 2*($B - $A) let $b := 2*($C - $B) return $a*(1 - $t) + $b }, $P0, $P1, $P2 )=>point:normalize() ) ) case "cubic" return ( (: : Derivative : Cubic P1 to P4 with controls P2, P3 : B(t) = (1-t)³·P1 + 3(1-t)²t·P2 + 3(1-t)t²·P3 + t³·P2 : B'(t) = (1-t)²·A + 2(1-t)t·B + t²·C : A=3(P2-P1), B=3(P3-P2), C=3(P4-P3) : B'(t) = at² + bt + c = 0 : a = A - 2B + C = 3(-P1 + 3P2 - 3P3 + P4) : b = 2(B - A) = 6(P1 - 2P2 + P3) : c = A = 3(P2 - P1) :) let $P1 := this:start($region) let $P2 := this:controls($region)[1] let $P3 := this:controls($region)[2] let $P4 := this:end($region) return ( point:map4( function ($p1 as xs:double, $p2 as xs:double, $p3 as xs:double, $p4 as xs:double) as xs:double { let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) let $c := 3*($p2 - $p1) return $a*$t*$t + $b*$t + $c }, $P1, $P2, $P3, $P4 ) )=>point:normalize() ) case "arc" return ( let $region := this:normalize-arc($region) let $c := this:arc-center($region) let $r := this:arc-radius($region) let $ellipse := ellipse:circle($c, $r) let $arc-angles := this:arc-angles($region) let $start-t := $arc-angles[1] div 360 let $end-t := $arc-angles[2] div 360 (: t for ellipse: ts + (te - ts)*ta :) let $revised-t := $start-t + ($end-t - $start-t)*$t let $p := $ellipse=>ellipse:ellipse-point($revised-t) let $angle := point:angle($c, $p) return ( this:edge( point:destination($p, $angle - 90, 1), point:destination($p, $angle + 90, 1) )=>point:normalize() ) ) case "ellipse" return ( (: May seem odd for this to be here, but edge depends on ellipse : and this is making an edge :) let $c := ellipse:center($region) let $p := $region=>ellipse:ellipse-point($t) let $angle := point:angle($c, $p) return ( this:edge( point:destination($p, $angle - 90, 1), point:destination($p, $angle + 90, 1) )=>point:normalize() ) ) default return errors:error("GEOM-NOTIMPLEMENTED", ("tangent", this:kind($region))) }; (:~ : curvature() : Return the curvature at the given point on the edge (2D) : κ(t) = |det(B(t),B''(t)|/||B'(t)||³ :) declare function this:curvature( $edge as map(xs:string,item()*), $t as xs:double ) as xs:double { abs(this:signed-curvature($edge, $t)) }; (:~ : signed-curvature() : Return the signed curvature at the given point on the edge (2D) : k(t) = det(B(t),B''(t)/||B'(t)||³ :) declare function this:signed-curvature( $edge as map(xs:string,item()*), $t as xs:double ) as xs:double { switch(this:kind($edge)) case "edge" return ( 0 ) case "quad" return ( (: : Derivative: : P0 = start, P1 = control, P2 = end : B(t) = (1-t)²·P0 + 2(1-t)t·P1 + t²·P2 : B(t) = (P0 - 2P1 + P2)t² + (-2P0 + 2P1)t + P0 : B'(t) = a(1-t) + b where a = 2(B-A), b=2(C-A) : B''(t) = -a where a=2(B-A) i.e. 2(A-B) : A=P0 B=P1 C=P2 :) let $P0 := this:start($edge)=>point:pcoordinates() let $P1 := (this:controls($edge)[1])=>point:pcoordinates() let $P2 := this:end($edge)=>point:pcoordinates() let $Bt := ( v:map3( function ($p0 as xs:double, $p1 as xs:double, $p2 as xs:double) as xs:double { ($p0 - 2*$p1 + $p2)*$t*$t + 2*($p1 - $p0) + $p0 }, $P0, $P1, $P2 ) ) let $Btder := ( v:map3( function ($p0 as xs:double, $p1 as xs:double, $p2 as xs:double) as xs:double { let $A := $p0 let $B := $p1 let $C := $p2 let $a := 2*($B - $A) let $b := 2*($C - $B) return $a*(1 - $t) + $b }, $P0, $P1, $P2 ) ) let $Btdoubleder := v:map2( function ($p0 as xs:double, $p1 as xs:double) as xs:double { 2*($p0 - $p1) }, $P0, $P1 ) return ( (: k(t) = det(B(t),B''(t)/||B'(t)||³ :) v:determinant($Bt, $Btdoubleder) div math:pow(v:magnitude($Btder), 3) ) ) case "cubic" return ( (: : Derivative : Cubic P1 to P4 with controls P2, P3 : B(t) = (1-t)³·P1 + 3(1-t)²t·P2 + 3(1-t)t²·P3 + t³·P2 : B'(t) = (1-t)²·A + 2(1-t)t·B + t²·C : A=3(P2-P1), B=3(P3-P2), C=3(P4-P3) : B'(t) = at² + bt + c = 0 : a = A - 2B + C = 3(-P1 + 3P2 - 3P3 + P4) : b = 2(B - A) = 6(P1 - 2P2 + P3) : c = A = 3(P2 - P1) : B''(t) = 2at + b :) let $P1 := this:start($edge)=>point:pcoordinates() let $P2 := (this:controls($edge)[1])=>point:pcoordinates() let $P3 := (this:controls($edge)[2])=>point:pcoordinates() let $P4 := this:end($edge)=>point:pcoordinates() let $Bt := ( v:map4( function ($p1 as xs:double, $p2 as xs:double, $p3 as xs:double, $p4 as xs:double) as xs:double { let $one-t := 1 - $t return ( math:pow($one-t, 3)*$p1 + 2*math:pow($one-t, 2)*$t*$p2 + 2*$one-t*math:pow($t, 2)*$p3 + math:pow($t, 3)*$p2 ) }, $P1, $P2, $P3, $P4 ) ) let $Btder := ( v:map4( function ($p1 as xs:double, $p2 as xs:double, $p3 as xs:double, $p4 as xs:double) as xs:double { let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) let $c := 3*($p2 - $p1) return $a*$t*$t + $b*$t + $c }, $P1, $P2, $P3, $P4 ) ) let $Btdoubleder := ( v:map4( function ($p1 as xs:double, $p2 as xs:double, $p3 as xs:double, $p4 as xs:double) as xs:double { let $a := 3*(-$p1 + 3*$p2 - 3*$p3 + $p4) let $b := 6*($p1 - 2*$p2 + $p3) return 2*$a*$t + $b }, $P1, $P2, $P3, $P4 ) ) return ( (: k(t) = det(B(t),B''(t)/||B'(t)||³ :) v:determinant($Bt, $Btdoubleder) div math:pow(v:magnitude($Btder), 3) ) ) case "arc" return if (this:arc-flipped($edge)) then -1 div this:arc-radius($edge) else 1 div this:arc-radius($edge) default return errors:error("GEOM-NOTIMPLEMENTED", ("curvature", this:kind($edge))) }; (:~ : osculating-circle() : Compute the osculating circle at the given point on the edge. : Returns a zero radius circle at the edge point for infinite or NaN : curvatures. :) declare function this:osculating-circle( $edge as map(xs:string,item()*), $t as xs:double ) as map(xs:string,item()*) { (: : : N(t) = normal unit vector : T'(t) = k(t)N(t) : k(t) is signed version of κ(t) : R(t) = 1/κ(t) : C(t) = B(t) + (1/κ²(t))T'(t) center of osculating circle : = B(t) + (1/κ²(t))k(t)N(t) : = B(t) + R²(t)(sign(k(t))/R(t))N(t) : = B(t) + sign(k(t))R(t)N(t) :) let $k := $edge=>this:signed-curvature($t) let $κ := abs($k) let $n := $edge=>this:tangent($t)=>point:perpendicular()=>point:normalize() let $p := $edge=>this:edge-point($t) return ( if (not($κ=$κ)) then ellipse:circle($p, 0) else if ($κ = xs:double("INF")) then ellipse:circle($p, 0) else ( let $radius := if ($κ = 0) then this:length($edge) div 2 else 1 div $κ let $sign := if ($κ = 0) then 1 else util:zsign($k) let $center := $n=>point:times($sign*$radius)=>point:add($p) return ( util:assert(not($radius=xs:double("INF")), "Bad radius: INF "||($κ=0)), util:assert($radius=$radius, "Bad radius: NaN"), ellipse:circle($center, $radius) ) ) ) }; (:====================================================================== : Interpolations :======================================================================:) declare function this:interpolate-edge-using( $edge as map(xs:string,item()*), $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { let $from := this:start($edge) let $to := this:end($edge) let $d := point:max-dimension(($from, $to)) for $t in $divisions($edge) order by $t ascending return ( point:map2( function ($a as xs:double, $b as xs:double) as xs:double { $a + $t * ($b - $a) }, $from, $to, $d ) ) }; (:~ : interpolate-edge() : Interpolate a linear edge. : : @param $n: number of interpolating points : @param $edge: the edge : @param $exclusive: exclude end point? default=false :) declare function this:interpolate-edge( $n as xs:integer, $edge as map(xs:string,item()*), $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-edge-using($edge, function ($region as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, $exclusive) } ) }; declare function this:interpolate-edge( $n as xs:integer, $edge as map(xs:string,item()*) ) as map(xs:string,item()*)* { this:interpolate-edge($n, $edge, false()) }; declare function this:interpolate-quadratic-using( $quadratic as map(xs:string,item()*), $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { let $from := $quadratic=>this:start() let $to := $quadratic=>this:end() let $controls := $quadratic=>this:controls() let $d := point:max-dimension(($from, $to, $controls[1])) for $t in $divisions($quadratic) order by $t ascending return ( point:map3( function ($a as xs:double, $b as xs:double, $c as xs:double) as xs:double { (1 - $t)*(1 - $t)*$a + 2*(1 - $t)*$t*$c + $t*$t*$b }, $from, $to, $controls[1], $d ) ) }; (:~ : interpolate-quadratic() : Interpolate a quadratic edge. : : B(t)=(1-t)²P0 + 2(1-t)tP1 + t²P2, 0 <= t <= 1 : : @param $n: number of interpolating points : @param $quadratic the edge : @param $exclusive: exclude end point? default=false :) declare function this:interpolate-quadratic( $n as xs:integer, $quadratic as map(xs:string,item()*), $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-quadratic-using($quadratic, function ($region as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, $exclusive) } ) }; declare function this:interpolate-quadratic( $n as xs:integer, $quadratic as map(xs:string,item()*) ) as map(xs:string,item()*)* { this:interpolate-quadratic($n, $quadratic, false()) }; declare function this:interpolate-cubic-using( $cubic as map(xs:string,item()*), $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { let $from := $cubic=>this:start() let $to := $cubic=>this:end() let $controls := $cubic=>this:controls() let $d := point:max-dimension(($from, $to, $controls[1], $controls[2])) for $t in $divisions($cubic) order by $t ascending return ( point:map4( function ($a as xs:double, $b as xs:double, $c as xs:double, $d as xs:double) as xs:double { (1 - $t)*(1 - $t)*(1 - $t)*$a + 3*(1 - $t)*(1 - $t)*$t*$c + 3*(1 - $t)*$t*$t*$d + $t*$t*$t*$b }, $from, $to, $controls[1], $controls[2], $d ) ) }; (:~ : interpolate-cubic() : Interpolate a cubic edge. : : B(t)=(1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3, 0 <= t <= 1 : : @param $n: number of interpolating points : @param $cubic the edge : @param $exclusive: exclude end point? default=false :) declare function this:interpolate-cubic( $n as xs:integer, $cubic as map(xs:string,item()*), $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-cubic-using($cubic, function ($region as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, $exclusive) } ) }; declare function this:interpolate-cubic( $n as xs:integer, $cubic as map(xs:string,item()*) ) as map(xs:string,item()*)* { this:interpolate-cubic($n, $cubic, false()) }; (:~ : interpolate-arc-using() : angle arc: break into sub-angles : point arc: point on circle => dest(center, angle, radius) : We only do angle interpolations; so convert to angle representation : : However: the center and start/end to use depends on large and flipped : We reflect around the line between start/end points when we drawing on : the arc from the other circle through the points : : Remapped degrees run from 0 to 360 : We need to adjust the angles to account for running backwards and : for crossing 0 : If flipped then reflect, unless we swapped, in which case reflect if we : aren't flipped. : : Cases: : from=200 to=10 large => to-from = -190 swap; 10 to 200; reverse : from=200 to=40 large => to-from = -160 to+=360; 200 to 400 : from=10 to=200 large => to-from = 190 10 to 200 : from=40 to=200 large => to-from = 160 swap; from+=360; 200 to 400; reverse : : from=200 to=10 !large => to-from = -190 to+=360; 200 to 370 : from=200 to=40 !large => to-from = -160 swap; 40 to 200; reverse : from=10 to=200 !large => to-from = 190 swap; from+=360; 200 to 370; reverse : from=40 to=200 !large => to-from = 160 40 to 200 :) declare function this:interpolate-arc-using( $arc as map(xs:string,item()*), $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { let $arc := this:normalize-arc($arc) let $center := $arc=>this:arc-center() let $radius := $arc=>this:arc-radius() let $angles := $arc=>this:arc-angles() let $from-angle := $angles[1] let $to-angle := $angles[2] let $da := $to-angle - $from-angle return ( for $t in $divisions($arc) order by $t ascending return ( point:destination($center, $from-angle + $t * $da, $radius) ) ) }; (:~ : interpolate-arc() : Interpolate a arc edge. : : @param $n: number of interpolating points : @param $arc the edge : @param $exclusive: exclude end point? default=false :) declare function this:interpolate-arc( $n as xs:integer, $arc as map(xs:string,item()*), $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-arc-using($arc, function ($region as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, $exclusive) } ) }; declare function this:interpolate-arc( $n as xs:integer, $arc as map(xs:string,item()*) ) as map(xs:string,item()*)* { this:interpolate-arc($n, $arc, false()) }; declare function this:interpolate-using( $edges as map(xs:string,item()*)*, $divisions as function(item()) as xs:double* ) as map(xs:string,item()*)* { for $edge in $edges return switch (this:kind($edge)) case "edge" return this:interpolate-edge-using($edge, $divisions) case "arc" return this:interpolate-arc-using($edge, $divisions) case "quad" return this:interpolate-quadratic-using($edge, $divisions) case "cubic" return this:interpolate-cubic-using($edge, $divisions) default return $edge }; (:~ : interpolate() : Interpolate the edges; return the interpolated points. : : @param $n: number of points of interpolation for each edge : @param $regions: the set of regions : @param $exclusive: include end point? default=false :) declare function this:interpolate( $n as xs:integer, $edges as map(xs:string,item()*)*, $exclusive as xs:boolean ) as map(xs:string,item()*)* { this:interpolate-using($edges, if ($exclusive) then ( function ($edge as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1, true()) } ) else ( function ($edge as map(xs:string,item()*)) as xs:double* { util:linspace($n, 0, 1) } ) ) }; declare function this:interpolate( $n as xs:integer, $edges as map(xs:string,item()*)* ) as map(xs:string,item()*)* { this:interpolate($n, $edges, false()) }; (:~ : is-straight-edge() : Test whether the edge is straight: either a linear edge or degenerate. :) declare function this:is-straight-edge( $edge as map(xs:string,item()*) ) as xs:boolean { switch (this:kind($edge)) case "edge" return true() case "quad" return point:sorientation(this:start($edge), this:controls($edge), this:end($edge))=0 or this:length($edge) = 0 case "cubic" return (point:sorientation(this:start($edge), this:controls($edge)[1], this:end($edge))=0 and point:sorientation(this:start($edge), this:controls($edge)[2], this:end($edge))=0) or this:length($edge) = 0 case "arc" return this:length($edge) = 0 default return errors:error("GEOM-BADREGION", ($edge, "is-straight-edge")) }; (:~ : is-straight() : Does the sequence of edges align? :) declare function this:is-straight($edges as map(xs:string,item()*)*) as xs:boolean { let $n := count($edges) return ( this:is-straight-edge(head($edges)) and ( every $i in 2 to $n satisfies ( this:is-straight-edge($edges[$i]) and point:sorientation(this:start($edges[$i - 1]), this:start($edges[$i]), this:end($edges[$i]))=0 ) ) ) }; declare function this:map-command( $kind as xs:string?, $relative as xs:string? ) as xs:string { switch ($relative) case "relative" return ( switch ($kind) case "goto" return "m" case "close" return "z" case "line" return "l" case "hr" return "h" case "vr" return "v" case "cubic" return "c" case "smooth_cubic" return "s" case "quad" return "q" case "smooth_quad" return "t" case "arc" return "a" case "ellipse-arc" return "a" default (: line :) return "l" ) default (: absolute :) return ( switch ($kind) case "goto" return "M" case "close" return "Z" case "line" return "L" case "hr" return "H" case "vr" return "V" case "cubic" return "C" case "smooth_cubic" return "S" case "quad" return "Q" case "smooth_quad" return "T" case "arc" return "A" case "ellipse-arc" return "A" default (: line :) return "L" ) }; declare function this:translate-edge( $edge as map(xs:string,item()*) ) as xs:string { string-join(( this:map-command(($edge("variety"),$edge("kind"))[1],"absolute"), switch(this:kind($edge)) case "arc" return ( (: XYZZY should swap start and end for large to get proper intent :) this:arc-radius($edge), (: rx :) this:arc-radius($edge), (: ry :) 0, (: rotate :) if (this:arc-large($edge)) then 1 else 0, (: large-arc :) if (this:arc-flipped($edge)) then 0 else 1 (: sweep :) ) case "ellipse-arc" return ( $edge("rx"), (: rx :) $edge("ry"), (: ry :) 0, (:rotate :) if (this:arc-large($edge)) then 1 else 0, (: large-arc :) if (this:arc-flipped($edge)) then 0 else 1 (: sweep :) ) case "quad" return ( point:px(this:controls($edge)[1]), (: c1.x :) point:py(this:controls($edge)[1]) (: c1.y :) ) case "cubic" return ( point:px(this:controls($edge)[1]), (: c1.x :) point:py(this:controls($edge)[1]), (: c1.y :) point:px(this:controls($edge)[2]), (: c2.x :) point:py(this:controls($edge)[2]) (: c2.y :) ) default return () , point:px(this:end($edge)), point:py(this:end($edge)) )!string(.)," ") }; declare %private variable $this:draw-edge-impl as function(*) := if ($config:DRAW-SNAPPED) then ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),("variety","type")))) let $start := this:start($item) let $end := this:end($item) let $d := this:weight($item) let $variety := ($item=>map:get("variety"),"line")[1] let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( <art:edge kind="{$variety}" x="{point:x($start)}" y="{point:y($start)}" x2="{point:x($end)}" y2="{point:y($end)}" >{ if (point:max-dimension(($start, $end)) > 2) then ( attribute z {point:z($start)}, attribute z2 {point:z($end)} ) else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) else ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),("variety","type")))) let $start := this:start($item) let $end := this:end($item) let $d := this:weight($item) let $variety := ($item=>map:get("variety"),"line")[1] let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( <art:edge kind="{$variety}" x="{point:px($start)}" y="{point:py($start)}" x2="{point:px($end)}" y2="{point:py($end)}" >{ if (point:max-dimension(($start, $end)) > 2) then ( attribute z {point:pz($start)}, attribute z2 {point:pz($end)} ) else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) ; declare %private variable $this:draw-quad-impl as function(*) := if ($config:DRAW-SNAPPED) then ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),("type")))) let $start := this:start($item) let $end := this:end($item) let $control := this:controls($item) let $d := this:weight($item) let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( <art:edge kind="quad" x="{point:x($start)}" y="{point:y($start)}" c1x="{point:x($control)}" c1y="{point:y($control)}" x2="{point:x($end)}" y2="{point:y($end)}" >{ if (point:max-dimension(($start,$end)) > 2) then ( attribute z {point:z($start)}, attribute z2 {point:z($end)} ) else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) else ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),("type")))) let $start := this:start($item) let $end := this:end($item) let $control := this:controls($item) let $d := this:weight($item) let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( <art:edge kind="quad" x="{point:px($start)}" y="{point:py($start)}" c1x="{point:px($control)}" c1y="{point:py($control)}" x2="{point:px($end)}" y2="{point:py($end)}" >{ if (point:max-dimension(($start,$end)) > 2) then ( attribute z {point:pz($start)}, attribute z2 {point:pz($end)} ) else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) ; declare %private variable $this:draw-cubic-impl as function(*) := if ($config:DRAW-SNAPPED) then ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),("type")))) let $start := this:start($item) let $end := this:end($item) let $controls := this:controls($item) let $d := this:weight($item) let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( <art:edge kind="quad" x="{point:x($start)}" y="{point:y($start)}" c1x="{point:x($controls[1])}" c1y="{point:y($controls[1])}" c2x="{point:x($controls[2])}" c2y="{point:y($controls[2])}" x2="{point:x($end)}" y2="{point:y($end)}" >{ if (point:max-dimension(($start,$end)) > 2) then ( attribute z {point:z($start)}, attribute z2 {point:z($end)} ) else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) else ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),("type")))) let $start := this:start($item) let $end := this:end($item) let $controls := this:controls($item) let $d := this:weight($item) let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( <art:edge kind="quad" x="{point:px($start)}" y="{point:py($start)}" c1x="{point:px($controls[1])}" c1y="{point:py($controls[2])}" c2x="{point:px($controls[2])}" c2y="{point:py($controls[2])}" x2="{point:px($end)}" y2="{point:py($end)}" >{ if (point:max-dimension(($start,$end)) > 2) then ( attribute z {point:pz($start)}, attribute z2 {point:pz($end)} ) else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) ; declare %private variable $this:draw-arc-impl as function(*) := if ($config:DRAW-SNAPPED) then ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),"type"))) let $center := $item=>this:arc-center() let $r := $item=>this:arc-radius() let $points := $item=>this:arc-ends() let $angles := $item=>this:arc-angles() let $start := if (exists($points)) then ( $points[1] ) else ( let $from := util:radians($angles[1]) return point:point( point:x($center)+math:cos($from)*$r, point:y($center)+math:sin($from)*$r ) ) let $end := if (exists($points)) then ( $points[2] ) else ( let $to := util:radians($angles[2]) return point:point( point:x($center)+math:cos($to)*$r, point:y($center)+math:sin($to)*$r ) ) let $d := this:weight($item) let $flipped := ($item=>this:arc-flipped(),false())[1] let $large := ($item=>this:arc-large(),false())[1] let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( (: No z on arcs right now :) <art:edge kind="arc" x="{point:x($start)}" y="{point:y($start)}" x2="{point:x($end)}" y2="{point:y($end)}" r="{round-half-to-even($r)}">{ if ($flipped) then attribute flipped {"true"} else (), if ($large) then attribute large {"true"} else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) else ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),"type"))) let $center := $item=>this:arc-center() let $r := $item=>this:arc-radius() let $points := $item=>this:arc-ends() let $angles := $item=>this:arc-angles() let $start := if (exists($points)) then ( $points[1] ) else ( let $from := util:radians($angles[1]) return point:point( point:px($center)+math:cos($from)*$r, point:py($center)+math:sin($from)*$r ) ) let $end := if (exists($points)) then ( $points[2] ) else ( let $to := util:radians($angles[2]) return point:point( point:px($center)+math:cos($to)*$r, point:py($center)+math:sin($to)*$r ) ) let $d := this:weight($item) let $flipped := ($item=>this:arc-flipped(),false())[1] let $large := ($item=>this:arc-large(),false())[1] let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( (: No z on arcs right now :) <art:edge kind="arc" x="{point:px($start)}" y="{point:py($start)}" x2="{point:px($end)}" y2="{point:py($end)}" r="{$r}">{ if ($flipped) then attribute flipped {"true"} else (), if ($large) then attribute large {"true"} else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) ; declare %private variable $this:draw-ellipse-arc-impl as function(*) := if ($config:DRAW-SNAPPED) then ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),"type"))) let $start := $item=>this:start() let $end := $item=>this:end() let $d := $item=>this:weight() let $flipped := ($item=>this:arc-flipped(),false())[1] let $large := ($item=>this:arc-large(),false())[1] let $ellipse := $item=>this:arc-ellipse() let $rotation := ellipse:rotation($ellipse) let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( (: Note: not handling rotations here: handled in main draw() function, sorta :) <art:edge kind="ellipse-arc" rx="{round-half-to-even(ellipse:rx($ellipse))}" ry="{round-half-to-even(ellipse:ry($ellipse))}" x="{point:x($start)}" y="{point:y($start)}" x2="{point:x($end)}" y2="{point:y($end)}" >{ (: No z on arcs right now :) if ($flipped) then attribute flipped {"true"} else (), if ($large) then attribute large {"true"} else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) else ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($properties, util:exclude(this:property-map($item),"type"))) let $start := $item=>this:start() let $end := $item=>this:end() let $d := $item=>this:weight() let $flipped := ($item=>this:arc-flipped(),false())[1] let $large := ($item=>this:arc-large(),false())[1] let $ellipse := $item=>this:arc-ellipse() let $rotation := ellipse:rotation($ellipse) let $type := ($item=>map:get("type"),"")[1] return ( if ($type="stroke") then ( <art:stroke-path>{ util:as-attributes($style-properties), $drawing("draw:draw")($item=>map:remove("type"), map {}, $drawing) (: Path gets style properties :) }</art:stroke-path> ) else ( (: Note: not handling rotations here: handled in main draw() function, sorta :) <art:edge kind="ellipse-arc" rx="{ellipse:rx($ellipse)}" ry="{ellipse:ry($ellipse)}" x="{point:px($start)}" y="{point:py($start)}" x2="{point:px($end)}" y2="{point:py($end)}" >{ (: No z on arcs right now :) if ($flipped) then attribute flipped {"true"} else (), if ($large) then attribute large {"true"} else (), util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("width")) then () else attribute width {$d} }</art:edge> ) ) } ) ; declare %private variable $this:draw-svg-edge-impl as function(*) := if ($config:DRAW-SNAPPED) then ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($drawing("draw:svg-style")($properties), $drawing("draw:svg-style")(util:exclude(this:property-map($item),("variety"))))) let $start := this:start($item) let $end := this:end($item) let $d := this:weight($item) let $path := this:map-command('goto','absolute')||" "|| point:x(this:start($item))||" "|| point:y(this:start($item))||" "|| this:translate-edge($item) return ( <svg:path d="{$path}">{ util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("stroke-width")) then () else attribute stroke-width {$d} }</svg:path> ) } ) else ( function( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { let $style-properties := util:merge-into(($drawing("draw:svg-style")($properties), $drawing("draw:svg-style")(util:exclude(this:property-map($item),("variety"))))) let $start := this:start($item) let $end := this:end($item) let $d := this:weight($item) let $path := this:map-command('goto','absolute')||" "|| point:px(this:start($item))||" "|| point:py(this:start($item))||" "|| this:translate-edge($item) return ( <svg:path d="{$path}">{ util:as-attributes($style-properties) , if ($d le 0 or $style-properties=>map:contains("stroke-width")) then () else attribute stroke-width {$d} }</svg:path> ) } ) ; declare function this:draw( $item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?) ) as item()* { if ($config:DRAWING-METHOD="art") then ( switch(this:kind($item)) case "edge" return $this:draw-edge-impl($item, $properties, $drawing) case "quad" return $this:draw-quad-impl($item, $properties, $drawing) case "cubic" return $this:draw-cubic-impl($item, $properties, $drawing) case "arc" return $this:draw-arc-impl($item, $properties, $drawing) case "ellipse-arc" return $this:draw-ellipse-arc-impl($item, $properties, $drawing) default return () ) else ( $this:draw-svg-edge-impl($item, $properties, $drawing) ) };