Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V3 geom2 no sides #1182

Merged
merged 15 commits into from
Jan 8, 2023
10 changes: 5 additions & 5 deletions packages/modeling/src/geometries/geom2/applyTransforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as vec2 from '../../maths/vec2/index.js'

/*
* Apply the transforms of the given geometry.
* NOTE: This function must be called BEFORE exposing any data. See toSides().
* NOTE: This function must be called BEFORE exposing any data. See toOutlines().
* @param {geom2} geometry - the geometry to transform
* @returns {geom2} the given geometry
*
Expand All @@ -14,10 +14,10 @@ export const applyTransforms = (geometry) => {
if (mat4.isIdentity(geometry.transforms)) return geometry

// apply transforms to each side
geometry.sides = geometry.sides.map((side) => {
const p0 = vec2.transform(vec2.create(), side[0], geometry.transforms)
const p1 = vec2.transform(vec2.create(), side[1], geometry.transforms)
return [p0, p1]
geometry.outlines = geometry.outlines.map((outline) => {
return outline.map((point) => {
return vec2.transform(vec2.create(), point, geometry.transforms)
})
})
geometry.transforms = mat4.create()
return geometry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { fromPoints } from './index.js'

import applyTransforms from './applyTransforms.js'

test('applyTransforms: Updates a populated geom2 with transformed sides', (t) => {
test('applyTransforms: Updates a populated geom2 with transforms', (t) => {
const points = [[0, 0], [1, 0], [0, 1]]
const expected = {
sides: [[[0, 1], [0, 0]], [[0, 0], [1, 0]], [[1, 0], [0, 1]]],
outlines: [[[0, 0], [1, 0], [0, 1]]],
transforms: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
const geometry = fromPoints(points)
Expand Down
4 changes: 2 additions & 2 deletions packages/modeling/src/geometries/geom2/clone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { clone, create, fromPoints } from './index.js'

test('clone: Creates a clone on an empty geom2', (t) => {
const expected = {
sides: [],
outlines: [],
z3dev marked this conversation as resolved.
Show resolved Hide resolved
transforms: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
const geometry = create()
Expand All @@ -16,7 +16,7 @@ test('clone: Creates a clone on an empty geom2', (t) => {
test('clone: Creates a clone of a complete geom2', (t) => {
const points = [[0, 0], [1, 0], [0, 1]]
const expected = {
sides: [[[0, 1], [0, 0]], [[0, 0], [1, 0]], [[1, 0], [0, 1]]],
outlines: [[[0, 0], [1, 0], [0, 1]]],
transforms: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
const geometry = fromPoints(points)
Expand Down
2 changes: 1 addition & 1 deletion packages/modeling/src/geometries/geom2/create.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import Vec2 from '../../maths/vec2/type'

export default create

declare function create(sides?: Array<[Vec2, Vec2]>): Geom2
declare function create(outlines?: Array<Array<Vec2>>): Geom2
17 changes: 7 additions & 10 deletions packages/modeling/src/geometries/geom2/create.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import * as mat4 from '../../maths/mat4/index.js'

/**
* Represents a 2D geometry consisting of a list of sides.
* Represents a 2D geometry consisting of a outlines, where each outline is an ordered list of points.
* @typedef {Object} geom2
* @property {Array} sides - list of sides, each side containing two points
* @property {mat4} transforms - transforms to apply to the sides, see transform()
* @property {Array} outlines - list of polygon outlines
* @property {mat4} transforms - transforms to apply to the geometry, see transform()
*/

/**
* Create a new 2D geometry composed of unordered sides (two connected points).
* @param {Array} [sides] - list of sides where each side is an array of two points
* Create a new 2D geometry composed of polygon outlines.
* @param {Array} [outlines] - list of outlines where each outline is an array of points
* @returns {geom2} a new geometry
* @alias module:modeling/geometries/geom2.create
*/
export const create = (sides) => {
if (sides === undefined) {
sides = [] // empty contents
}
export const create = (outlines = []) => {
return {
sides: sides,
outlines,
transforms: mat4.create()
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/modeling/src/geometries/geom2/create.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import { create } from './index.js'

test('create: Creates an empty geom2', (t) => {
const expected = {
sides: [],
outlines: [],
transforms: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
t.deepEqual(create(), expected)
})

test('create: Creates a populated geom2', (t) => {
const sides = [[[0, 0], [1, 1]]]
const points = [[0, 0], [1, 0], [0, 1]]
const expected = {
sides: sides,
outlines: [[[0, 0], [1, 0], [0, 1]]],
transforms: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
t.deepEqual(create(sides), expected)
t.deepEqual(create([points]), expected)
})
18 changes: 14 additions & 4 deletions packages/modeling/src/geometries/geom2/fromCompactBinary.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,21 @@ export const fromCompactBinary = (data) => {

created.transforms = mat4.clone(data.slice(1, 17))

for (let i = 21; i < data.length; i += 4) {
const point0 = vec2.fromValues(data[i + 0], data[i + 1])
const point1 = vec2.fromValues(data[i + 2], data[i + 3])
created.sides.push([point0, point1])
for (let i = 21; i < data.length;) {
const length = data[i++] // number of points for this polygon
if (length < 0 || i + length * 2 > data.length) {
throw new Error('invalid compact binary data')
}
const outline = []
for (let j = 0; j < length; j++) {
const x = data[i + j * 2]
const y = data[i + j * 2 + 1]
outline.push(vec2.fromValues(x, y))
}
created.outlines.push(outline)
i += length * 2
}

// transfer known properties, i.e. color
if (data[17] >= 0) {
created.color = [data[17], data[18], data[19], data[20]]
Expand Down
6 changes: 0 additions & 6 deletions packages/modeling/src/geometries/geom2/fromOutlines.d.ts

This file was deleted.

36 changes: 0 additions & 36 deletions packages/modeling/src/geometries/geom2/fromOutlines.js

This file was deleted.

13 changes: 4 additions & 9 deletions packages/modeling/src/geometries/geom2/fromPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,11 @@ export const fromPoints = (points) => {
throw new Error('the given points must define a closed geometry with three or more points')
platypii marked this conversation as resolved.
Show resolved Hide resolved
}
// adjust length if the given points are closed by the same point
if (vec2.equals(points[0], points[length - 1])) --length

const sides = []
let prevpoint = points[length - 1]
for (let i = 0; i < length; i++) {
const point = points[i]
sides.push([vec2.clone(prevpoint), vec2.clone(point)])
z3dev marked this conversation as resolved.
Show resolved Hide resolved
prevpoint = point
if (vec2.equals(points[0], points[length - 1])) {
points = points.slice(0, points.length - 1)
}
return create(sides)

return create([points])
}

export default fromPoints
15 changes: 11 additions & 4 deletions packages/modeling/src/geometries/geom2/fromPoints.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import { fromPoints } from './index.js'
test('fromPoints: creates populated geom2', (t) => {
const points = [[0, 0], [1, 0], [0, 1]]
const expected = {
sides: [[[0, 1], [0, 0]], [[0, 0], [1, 0]], [[1, 0], [0, 1]]],
outlines: [[[0, 0], [1, 0], [0, 1]]],
transforms: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
t.deepEqual(fromPoints(points), expected)

const points2 = [[0, 0], [1, 0], [0, 1], [0, 0]]
t.deepEqual(fromPoints(points2), expected)
})

test('fromPoints: throws for improper points', (t) => {
Expand All @@ -20,3 +17,13 @@ test('fromPoints: throws for improper points', (t) => {
t.throws(() => fromPoints([]), { instanceOf: Error })
t.throws(() => fromPoints([[0, 0]]), { instanceOf: Error })
})

test('fromPoints: remove duplicate start/end', (t) => {
const points = [[0, 0], [2, 0], [0, 3], [0, 0]]
const expected = {
outlines: [[[0, 0], [2, 0], [0, 3]]],
transforms: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
t.deepEqual(fromPoints(points), expected)
t.is(points.length, 4) // don't mutate the input array
})
6 changes: 6 additions & 0 deletions packages/modeling/src/geometries/geom2/fromSides.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Geom2 from './type'
import Vec2 from '../../maths/vec2/type'

export default fromSides

declare function fromSides(sides: Array<[Vec2, Vec2]>): Geom2
116 changes: 116 additions & 0 deletions packages/modeling/src/geometries/geom2/fromSides.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as vec2 from '../../maths/vec2/index.js'
import create from './create.js'

/*
* Create a list of edges which SHARE vertices.
* This allows the edges to be traversed in order.
*/
const toSharedVertices = (sides) => {
const unique = new Map() // {key: vertex}
const getUniqueVertex = (vertex) => {
const key = vertex.toString()
if (unique.has(key)) {
return unique.get(key)
} else {
unique.set(key, vertex)
return vertex
}
}

return sides.map((side) => side.map(getUniqueVertex))
}

/*
* Convert a list of sides into a map from vertex to edges.
*/
const toVertexMap = (sides) => {
const vertexMap = new Map()
// first map to edges with shared vertices
const edges = toSharedVertices(sides)
// construct adjacent edges map
edges.forEach((edge) => {
if (vertexMap.has(edge[0])) {
vertexMap.get(edge[0]).push(edge)
} else {
vertexMap.set(edge[0], [edge])
}
})
return vertexMap
}

/**
* Create a new 2D geometry from a list of sides.
* @param {Array} sides - list of sides to create outlines from
* @returns {geom2} a new geometry
*
* @example
* let geometry = fromSides([[[0, 0], [1, 0]], [[1, 0], [1, 1]], [[1, 1], [0, 0]]])
*/
export const fromSides = (sides) => {
const vertexMap = toVertexMap(sides) // {vertex: [edges]}
const outlines = []
while (true) {
let startSide
for (const [vertex, edges] of vertexMap) {
startSide = edges.shift()
if (!startSide) {
vertexMap.delete(vertex)
continue
}
break
}
if (startSide === undefined) break // all starting sides have been visited

const connectedVertexPoints = []
const startVertex = startSide[0]
while (true) {
connectedVertexPoints.push(startSide[0])
const nextVertex = startSide[1]
if (nextVertex === startVertex) break // the outline has been closed
const nextPossibleSides = vertexMap.get(nextVertex)
if (!nextPossibleSides) {
throw new Error(`geometry is not closed at vertex ${nextVertex}`)
}
const nextSide = popNextSide(startSide, nextPossibleSides)
if (nextPossibleSides.length === 0) {
vertexMap.delete(nextVertex)
}
startSide = nextSide
} // inner loop

// due to the logic of fromPoints()
// move the first point to the last
if (connectedVertexPoints.length > 0) {
connectedVertexPoints.push(connectedVertexPoints.shift())
}
outlines.push(connectedVertexPoints)
} // outer loop
vertexMap.clear()
return create(outlines)
}

// find the first counter-clockwise edge from startSide and pop from nextSides
const popNextSide = (startSide, nextSides) => {
if (nextSides.length === 1) {
return nextSides.pop()
}
const v0 = vec2.create()
const startAngle = vec2.angleDegrees(vec2.subtract(v0, startSide[1], startSide[0]))
let bestAngle
let bestIndex
nextSides.forEach((nextSide, index) => {
const nextAngle = vec2.angleDegrees(vec2.subtract(v0, nextSide[1], nextSide[0]))
let angle = nextAngle - startAngle
if (angle < -180) angle += 360
if (angle >= 180) angle -= 360
if (bestIndex === undefined || angle > bestAngle) {
bestIndex = index
bestAngle = angle
}
})
const nextSide = nextSides[bestIndex]
nextSides.splice(bestIndex, 1) // remove side from list
return nextSide
}

export default fromSides
Loading