Skip to content

Commit

Permalink
evenodd path fill, exclude contained components, better contained-reg…
Browse files Browse the repository at this point in the history
…ion/component handling
  • Loading branch information
ryan-williams committed Oct 7, 2023
1 parent 748e253 commit 54ac0ec
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 61 deletions.
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"email": "ryan@runsascoded.com"
},
"dependencies": {
"apvd": "https://gitpkg.now.sh/runsascoded/shapes?1e0bd5c",
"apvd": "https://gitpkg.now.sh/runsascoded/shapes?9928b7c",
"bootstrap": "^5.3.1",
"buffer": "^6.0.3",
"lodash": "^4.17.21",
Expand Down
10 changes: 10 additions & 0 deletions pages/ellipse-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ export function Body() {
return <div className={css.body}>
<div className={`${css.row} ${css.content}`}>
<Grid className={css.grid} state={gridState}>
<ellipse
cx={1.3345311198605432}
cy={2.8430120216925644e-16}
rx={2.362476333635918}
ry={2.8207141903440474}
fillOpacity={0.3}
fill={"green"}
/>
<circle cx={0} cy={0} r={1} />

<path
d={`${containerPath} ${subregions.map(([ path ]) => path).join(" ")}`}
fillRule={"evenodd"}
Expand Down
37 changes: 10 additions & 27 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Grid, {GridState} from "../src/components/grid"
import React, {DetailedHTMLProps, Dispatch, HTMLAttributes, ReactNode, useCallback, useEffect, useMemo, useRef, useState} from "react"
import * as apvd from "apvd"
import {train, update_log_level} from "apvd"
import {makeModel, Model, Region, Step} from "../src/lib/regions"
import {makeModel, Model, Region, regionPath, Step} from "../src/lib/regions"
import {Point} from "../src/components/point"
import css from "./index.module.scss"
import A from "next-utils/a"
Expand All @@ -20,7 +20,7 @@ import {getMidpoint, getPointAndDirectionAtTheta, getRegionCenter} from "../src/
import {BoundingBox, getRadii, mapShape, S, Shape, shapeBox, Shapes, shapesParam, shapeStrJS, shapeStrJSON, shapeStrRust} from "../src/lib/shape";
import {TargetsTable} from "../src/components/tables/targets";
import {makeTargets, Target, Targets, targetsParam} from "../src/lib/targets";
import {Disjoint, Ellipses4, Ellipses4t, InitialLayout, CirclesFlexible, toShape, CirclesFixed, Concentric} from "../src/lib/layout";
import {Disjoint, Ellipses4, Ellipses4t, InitialLayout, CirclesFlexible, toShape, CirclesFixed, Nested} from "../src/lib/layout";
import {VarsTable} from "../src/components/tables/vars";
import {SparkLineProps} from "../src/components/spark-lines";
import {CircleCoords, Coord, makeVars, Vars, XYRRCoords, XYRRTCoords} from "../src/lib/vars";
Expand Down Expand Up @@ -257,7 +257,7 @@ const layouts: LinkItem<InitialLayout>[] = [
{ name: "Circles (flexible)", val: CirclesFlexible, description: "4 ellipses, initialized as circles, and oriented in a diamond configuration, such that 2 different subsets (of 3) are symmetric, and 11 of 15 possible regions are represented (missing 2 4C2's and 2 4C3's).", },
{ name: "Circles (fixed)", val: CirclesFixed, description: "4 circles, initialized in a diamond as in \"Circles (flexible)\" above, but these are fixed as circles (rx and ry remain constant, rotation is immaterial)", },
{ name: "Disjoint", val: Disjoint, description: "4 disjoint circles. When two (or more) sets are supposed to intersect, but don't, a synthetic penalty is added to the error computation, which is proportional to: 1) each involved set's distance to the centroid of the centers of the sets that are supposed to intersect, as well as 2) the size of the target subset. This \"disjoint\" initial layout serves demonstrate/test this behavior. More sophisticated heuristics would be useful here, as the current scheme is generally insufficient to coerce all sets into intersecting as they should." },
{ name: "Concentric", val: Concentric, description: "4 concentric circles, stress test disjoint/contained region handling" },
{ name: "Nested", val: Nested, description: "4 nested circles, stresses disjoint/contained region handling, which has known issues!" },
{ name: "Variant callers (best)", val: "#s=Mzxv4Cc95664TAhIgtTaZ1wTbpB32hca6RnYrxzN5QRgbF4oaXr5MStC6KxNYYZy5g5IuzaS1moF4lLWtIXXY-VOO2f8wNvsQk9Jqqfg0B-RDkXMZTCTpTaymPnuwF-vswFGRVwFE4hgScC1ofXRaBdnvzm84fjZ8wtEkWHaqiifUM4TVEtIbh8&t=633,618,112,187,0,14,1,319,13,55,17,21,0,9,36", description: <>Best computed layout for the "variant callers" example, from {VariantCallersPaperLink}. ≈50,000 steps beginning from the "Circles" layout above, error &lt;0.176%.</>},
{ name: "Variant callers (alternate)", val: "#s=MzC1VAFocttl2gbaDkR1obVIOSo-npdk8mfAn4j0s68wpq4FE4o0YIptFI5hupi525mqCJLTS0BbLsnqcJ0oFOtaun28Afy9HfyAHhdHhtsAsLO8mNdyKFNwt4op_97d4DXguxY3S4k7RxPbNbPIu_2XIvm5qJ0NJn5qsgeVxEhvcgoRO8FFnpU&t=633,618,112,187,0,14,1,319,13,55,17,21,0,9,36", description: <>Another layout for the "variant callers" example, from {VariantCallersPaperLink}. ≈20,000 steps beginning from the "Ellipses" layout above, error ≈2.27%.</>}
// { name: "CircleLattice", layout: SymmetricCircleLattice, description: "4 circles centered at (0,0), (0,1), (1,0), (1,1)", },
Expand Down Expand Up @@ -383,7 +383,7 @@ export function Body() {
() => {
const targets = rawTargets
const { numShapes } = targets
const initialSets =
const initialSets: S[] =
initialShapes
.slice(0, numShapes)
.map((s, idx) => {
Expand Down Expand Up @@ -507,9 +507,10 @@ export function Body() {
() => {
if (typeof window !== 'undefined') {
window.model = model
window.curStep = curStep
}
},
[ model,]
[ model, curStep ]
)

const getHistoryState = useCallback(
Expand Down Expand Up @@ -1219,28 +1220,9 @@ export function Body() {
const regionPaths = useMemo(
() =>
curStep && <g id={"regionPaths"}>{
curStep.regions.map(({ key, segments}, regionIdx) => {
let d = ''
segments.forEach(({edge, fwd}, idx) => {
const { set: { shape }, node0, node1, theta0, theta1, } = edge
const [rx, ry] = getRadii(shape)
const theta = shape.kind === 'XYRRT' ? shape.t : 0
const degrees = theta * 180 / PI
const [startNode, endNode] = fwd ? [node0, node1] : [node1, node0]
const start = {x: startNode.x.v, y: startNode.y.v}
const end = {x: endNode.x.v, y: endNode.y.v}
if (idx == 0) {
d = `M ${start.x} ${start.y}`
}
// console.log("edge:", edge, "fwd:", fwd, "theta0:", theta0, "theta1:", theta1, "start:", start, "end:", end, "shape:", shape, "degrees:", degrees)
if (segments.length == 1) {
const mid = getMidpoint(edge, 0.4)
d += ` A ${rx},${ry} ${degrees} 0 ${fwd ? 1 : 0} ${mid.x},${mid.y}`
d += ` A ${rx},${ry} ${degrees} 1 ${fwd ? 1 : 0} ${end.x},${end.y}`
} else {
d += ` A ${rx},${ry} ${degrees} ${theta1 - theta0 > PI ? 1 : 0} ${fwd ? 1 : 0} ${end.x},${end.y}`
}
})
curStep.regions.map((region, regionIdx) => {
const { key, segments} = region
const d = regionPath(region)
const isHovered = hoveredRegion == key
return (
<path
Expand All @@ -1251,6 +1233,7 @@ export function Body() {
strokeWidth={1 / scale}
fill={"grey"}
fillOpacity={isHovered ? 0.4 : 0}
fillRule={"evenodd"}
onMouseOver={() => setHoveredRegion(key)}
// onMouseLeave={() => setHoveredRegion(null)}
onMouseOut={() => setHoveredRegion(null)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,14 @@ export default function Grid({ handleMouseDown, handleMouseMove, handleDrag, han
<svg
ref={svg}
viewBox={`0 0 ${width} ${height}`}
className={`${css.grid} ${svgClassName || ''}`}
className={`${css.grid || ''} ${svgClassName || ''}`}
style={{ height }}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
>
<g
className={`${css.projection}`}
className={css.projection}
transform={transforms.map(([type, x, y]) => `${type}(${x},${y})`).join(" ")}
>
{gridLines}
Expand Down
9 changes: 5 additions & 4 deletions src/lib/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ export const Disjoint: InitialLayout = [
{ c: { x: 0, y: 3, }, r: { x: 1, y: 1 }, t: 0, },
]

export const Concentric: InitialLayout = [
{ c: { x: 0, y: 0, }, r: { x: 1, y: 1 }, t: 0, },
// TODO: if they actually share a center, the missing region penalty heuristics go to NaN
export const Nested: InitialLayout = [
{ c: { x: 0 , y: 0, }, r: { x: 1, y: 1 }, t: 0, },
{ c: { x: 0.5, y: 0, }, r: { x: 2, y: 2 }, t: 0, },
{ c: { x: 0, y: 0, }, r: { x: 3, y: 3 }, t: 0, },
{ c: { x: 0, y: 0, }, r: { x: 4, y: 4 }, t: 0, },
{ c: { x: 1 , y: 0, }, r: { x: 3, y: 3 }, t: 0, },
{ c: { x: 1.5, y: 0, }, r: { x: 4, y: 4 }, t: 0, },
]

export const SymmetricCircleLattice: InitialLayout = [
Expand Down
108 changes: 86 additions & 22 deletions src/lib/regions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import * as apvd from "apvd";
import {Dual, Error, Targets} from "apvd"
import {S, Set, Shape} from "./shape";
import {Dual, Targets} from "apvd"
import {getRadii, S, Set, Shape} from "./shape";
import {PI} from "./math";
import {getMidpoint} from "./region";

export type Point = {
x: Dual
y: Dual
edges: Edge[]
}

export type Errors = Map<string, Error>
export type Errors = Map<string, apvd.Error>

export type Step = {
sets: S[]
Expand Down Expand Up @@ -49,13 +51,26 @@ export function makeStep(step: apvd.Step, initialSets: Set[]): Step {
const { components, errors, ...rest } = step
const sets: S[] = []
components.forEach(c => c.sets.forEach(({ idx, shape }) => {
sets[idx] = { ...initialSets[idx], shape: makeShape(shape) }
sets[idx] = { ...initialSets[idx], shape: makeShape(shape), }
}))
const newComponents = components.map(c => makeComponent(c, sets))
const newComponentsMap = new Map(newComponents.map(c => [c.key, c]))
// console.log("initial sets:", sets)
const points = newComponents.flatMap(c => c.points)
const edges = newComponents.flatMap(c => c.edges)
const regions = newComponents.flatMap(c => c.regions)
newComponents.forEach((newComponent, idx) => {
const apvdComponent = components[idx]
apvdComponent.regions.forEach((apvdRegion, regionIdx) => {
const newRegion = newComponent.regions[regionIdx]
// console.log("region:", apvdRegion, "children:", apvdRegion.child_component_keys)
newRegion.childComponents = apvdRegion.child_component_keys.map(key => {
const newComponent = newComponentsMap.get(key)
if (!newComponent) throw Error(`no component with key ${key}; referenced in region ${apvdRegion.key} of component ${apvdComponent.key}`)
return newComponent
})
})
})
return {
sets,
points,
Expand All @@ -81,41 +96,88 @@ export function makeShape(shape: apvd.Shape<number>): Shape<number> {
}
}

export function makeRegion(region: apvd.Region, allSets: S[], edges: Edge[]): Region {
// console.log("makeRegion:", region)
const segments = region.segments.map(({ edge_idx, fwd }) => ({
edge: edges[edge_idx],
fwd,
}))
const containers = region.container_set_idxs.map(idx => allSets[idx])
return {
key: region.key,
segments,
area: region.area,
containers,
childComponents: [],
}
}

export function makeComponent(component: apvd.Component, allSets: S[]): Component {
// console.log("makeComponent:", component)
const points: Point[] = component.points.map(({ p: { x, y }}) => ({
x, y,
edges: []
}))
const edges: Edge[] = component.edges.map(({ set_idx, node0_idx, node1_idx, theta0, theta1, container_idxs, is_component_boundary, }) => ({
set: allSets[set_idx],
node0: points[node0_idx],
node1: points[node1_idx],
theta0, theta1,
containers: container_idxs.map(setIdx => allSets[setIdx]),
isComponentBoundary: is_component_boundary,
}))
const edges: Edge[] = component.edges.map(({ set_idx, node0_idx, node1_idx, theta0, theta1, container_idxs, is_component_boundary, }) => {
const node0 = points[node0_idx]
const node1 = points[node1_idx]
if (!node0 || !node1) {
throw Error(`node0 or node1 is null: ${node0_idx} ${node1_idx}, ${node0} ${node1}`)
}
return ({
set: allSets[set_idx],
node0: points[node0_idx],
node1: points[node1_idx],
theta0, theta1,
containers: container_idxs.map(setIdx => allSets[setIdx]),
isComponentBoundary: is_component_boundary,
})
})
component.points.forEach(({ edge_idxs }, pointIdx) => {
const point = points[pointIdx]
point.edges = edge_idxs.map(edgeIdx => edges[edgeIdx])
})
const regions: Region[] = component.regions.map(({ key, segments, area, container_idxs }) => ({
key,
segments: segments.map(({ edge_idx, fwd }) => ({
edge: edges[edge_idx],
fwd,
})),
area,
containers: container_idxs.map((cidx: number) => allSets[cidx]),
}))
return { sets: component.sets.map(({ idx }) => allSets[idx]), points, edges, regions, }
const regions: Region[] = component.regions.map(r => makeRegion(r, allSets, edges))
// const containers = component.container_idxs.map((cidx: number) => allSets[cidx])
const sets = component.sets.map(({ idx }) => allSets[idx])
const hull = makeRegion(component.hull, allSets, edges)
return { key: component.key, sets, points, edges, regions, /*containers, */hull, }
}

export type Region = {
key: string
segments: Segment[]
area: Dual
containers: S[]
childComponents: Component[]
}

export function regionPath({ segments, childComponents, }: Region): string {
let d = ''
segments.forEach(({edge, fwd}, idx) => {
const { set: { shape }, node0, node1, theta0, theta1, } = edge
const [rx, ry] = getRadii(shape)
const theta = shape.kind === 'XYRRT' ? shape.t : 0
const degrees = theta * 180 / PI
const [startNode, endNode] = fwd ? [node0, node1] : [node1, node0]
const start = { x: startNode.x.v, y: startNode.y.v }
const end = { x: endNode.x.v, y: endNode.y.v }
if (idx == 0) {
d = `M ${start.x} ${start.y}`
}
// console.log("edge:", edge, "fwd:", fwd, "theta0:", theta0, "theta1:", theta1, "start:", start, "end:", end, "shape:", shape, "degrees:", degrees)
if (segments.length == 1) {
const mid = getMidpoint(edge, 0.4)
d += ` A ${rx},${ry} ${degrees} 0 ${fwd ? 1 : 0} ${mid.x},${mid.y}`
d += ` A ${rx},${ry} ${degrees} 1 ${fwd ? 1 : 0} ${end.x},${end.y}`
} else {
d += ` A ${rx},${ry} ${degrees} ${theta1 - theta0 > PI ? 1 : 0} ${fwd ? 1 : 0} ${end.x},${end.y}`
}
})
if (childComponents?.length) {
d += ` ${childComponents.map(c => regionPath(c.hull)).join(' ')}`
}
return d
}

export interface Segment {
Expand All @@ -134,8 +196,10 @@ export type Edge = {
}

export type Component = {
key: string
sets: S[]
points: Point[]
edges: Edge[]
regions: Region[]
hull: Region
}
1 change: 1 addition & 0 deletions src/lib/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {R2} from "apvd";
import {abs, cos, max, sin} from "./math";
import {Param} from "next-utils/params";
import ShapesBuffer, {Opts, ShapesParam} from "./shapes-buffer";
import {Component} from "./regions";

export interface Circle<D> {
kind: 'Circle'
Expand Down

0 comments on commit 54ac0ec

Please sign in to comment.