Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jsdoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"packages/modeling/src/geometries/geom2",
"packages/modeling/src/geometries/geom3",
"packages/modeling/src/geometries/path2",
"packages/modeling/src/geometries/path3",
"packages/modeling/src/geometries/poly2",
"packages/modeling/src/geometries/poly3",
"packages/modeling/src/geometries/slice",
Expand Down
10 changes: 1 addition & 9 deletions packages/modeling/src/geometries/geom3/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,4 @@ import * as mat4 from '../../maths/mat4/index.js'
* @returns {Geom3} a new geometry
* @alias module:modeling/geometries/geom3.create
*/
export const create = (polygons) => {
if (polygons === undefined) {
polygons = [] // empty contents
}
return {
polygons,
transforms: mat4.create()
}
}
export const create = (polygons = []) => ({ polygons, transforms: mat4.create() })
1 change: 1 addition & 0 deletions packages/modeling/src/geometries/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * as geom2 from './geom2/index.js'
export * as geom3 from './geom3/index.js'
export * as path2 from './path2/index.js'
export * as path3 from './path3/index.js'
export * as poly2 from './poly2/index.js'
export * as poly3 from './poly3/index.js'
export * as slice from './slice/index.js'
1 change: 1 addition & 0 deletions packages/modeling/src/geometries/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
export * as geom2 from './geom2/index.js'
export * as geom3 from './geom3/index.js'
export * as path2 from './path2/index.js'
export * as path3 from './path3/index.js'
export * as poly2 from './poly2/index.js'
export * as poly3 from './poly3/index.js'
export * as slice from './slice/index.js'
11 changes: 1 addition & 10 deletions packages/modeling/src/geometries/path2/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,4 @@ import * as mat4 from '../../maths/mat4/index.js'
* @example
* let newPath = create()
*/
export const create = (points) => {
if (points === undefined) {
points = []
}
return {
points: points,
isClosed: false,
transforms: mat4.create()
}
}
export const create = (points = []) => ({ points: points, isClosed: false, transforms: mat4.create() })
22 changes: 22 additions & 0 deletions packages/modeling/src/geometries/path3/applyTransforms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as mat4 from '../../maths/mat4/index.js'
import * as vec3 from '../../maths/vec3/index.js'

/*
* Apply the transforms of the given geometry.
*
* NOTE: This function must be called BEFORE exposing any data. See toVertices.
*
* @param {Path3} geometry - the geometry to transform
* @returns {Path3} the given geometry
* @function
*
* @example
* geometry = applyTransforms(geometry)
*/
export const applyTransforms = (geometry) => {
if (mat4.isIdentity(geometry.transforms)) return geometry

geometry.vertices = geometry.vertices.map((vertex) => vec3.transform(vec3.create(), vertex, geometry.transforms))
geometry.transforms = mat4.create()
return geometry
}
28 changes: 28 additions & 0 deletions packages/modeling/src/geometries/path3/applyTransforms.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import test from 'ava'

import { fromVertices } from './index.js'

import { applyTransforms } from './applyTransforms.js'

import { comparePoints, compareVectors } from '../../../test/helpers/index.js'

test('applyTransforms: Updates a populated path with transformed points', (t) => {
const vertices = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]
const expected = {
vertices: [[0, 0, 0], [1, 0, 0], [0, 1, 0]],
isClosed: false,
transforms: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
const geometry = fromVertices({}, vertices)
const updated = applyTransforms(geometry)
t.is(geometry, updated)
t.true(comparePoints(updated.vertices, expected.vertices))
t.false(updated.isClosed)
t.true(compareVectors(updated.transforms, expected.transforms))

const updated2 = applyTransforms(updated)
t.is(updated, updated2)
t.true(comparePoints(updated2.vertices, expected.vertices))
t.false(updated2.isClosed)
t.true(compareVectors(updated2.transforms, expected.transforms))
})
3 changes: 3 additions & 0 deletions packages/modeling/src/geometries/path3/close.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Path3 } from './type.d.ts'

export function close(geometry: Path3): Path3
31 changes: 31 additions & 0 deletions packages/modeling/src/geometries/path3/close.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { EPS } from '../../maths/constants.js'

import * as vec3 from '../../maths/vec3/index.js'

/**
* Close the given geometry.
*
* @param {Path3} geometry - the path to close
* @returns {Path3} a new path
* @function
* @alias module:modeling/geometries/path3.close
*/
export const close = (geometry) => {
if (geometry.isClosed) return geometry

const cloned = Object.assign({}, geometry)
cloned.isClosed = true

if (cloned.vertices.length > 1) {
// make sure the paths are formed properly
const vertices = cloned.vertices
const p0 = vertices[0]
let pn = vertices[vertices.length - 1]
while (vec3.distance(p0, pn) < (EPS * EPS)) {
vertices.pop()
if (vertices.length === 1) break
pn = vertices[vertices.length - 1]
}
}
return cloned
}
43 changes: 43 additions & 0 deletions packages/modeling/src/geometries/path3/close.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import test from 'ava'

import { close, create, fromVertices } from './index.js'

test('close: closes an empty path', (t) => {
const p1 = create()
t.false(p1.isClosed)

const p2 = close(p1)
t.true(p2.isClosed)
t.not(p1, p2)

const p3 = close(p2)
t.true(p3.isClosed)
t.is(p2, p3)
})

test('close: closes various paths', (t) => {
let p1 = create()
p1 = close(p1)
t.true(p1.isClosed)
t.is(0, p1.vertices.length)

let p2 = fromVertices({ closed: false }, [])
p2 = close(p2)
t.true(p2.isClosed)
t.is(0, p2.vertices.length)

let p3 = fromVertices({ closed: true }, [[0, 0, 0]])
p3 = close(p3)
t.true(p3.isClosed)
t.is(1, p3.vertices.length)

let p4 = fromVertices({ closed: true }, [[0, 0, 0], [0, 0, 0]])
p4 = close(p4)
t.true(p4.isClosed)
t.is(1, p4.vertices.length) // the last point is removed

let p5 = fromVertices({ closed: true }, [[0, 0, 0], [1, 1, 0], [0, 0, 0]])
p5 = close(p5)
t.true(p5.isClosed)
t.is(2, p5.vertices.length) // the last point is removed
})
3 changes: 3 additions & 0 deletions packages/modeling/src/geometries/path3/concat.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Path3 } from './type.d.ts'

export function concat(...paths: Array<Path3>): Path3
36 changes: 36 additions & 0 deletions packages/modeling/src/geometries/path3/concat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { equals } from '../../maths/vec3/index.js'

import { fromVertices } from './fromVertices.js'
import { toVertices } from './toVertices.js'

/**
* Concatenate the given paths.
*
* If both contain the same vertex at the junction, merge it into one.
* A concatenation of zero paths is an empty, open path.
* A concatenation of one closed path to a series of open paths produces a closed path.
* A concatenation of a path to a closed path is an error.
*
* @param {...Path3} paths - the paths to concatenate
* @returns {Path3} a new path
* @function
* @alias module:modeling/geometries/path3.concat
*
* @example
* let newPath = concat(fromVertices({}, [[1, 2, 3]]), fromVertices({}, [[4, 5, 6]]))
*/
export const concat = (...paths) => {
// Only the last path can be closed, producing a closed path.
let isClosed = false
let vertices = []
paths.forEach((path, i) => {
const tmp = toVertices(path).slice()
if (vertices.length > 0 && tmp.length > 0 && equals(tmp[0], vertices[vertices.length - 1])) tmp.shift()
if (tmp.length > 0 && isClosed) {
throw new Error(`Cannot concatenate to a closed path; check the ${i}th path`)
}
isClosed = path.isClosed
vertices = vertices.concat(tmp)
})
return fromVertices({ closed: isClosed }, vertices)
}
35 changes: 35 additions & 0 deletions packages/modeling/src/geometries/path3/concat.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import test from 'ava'

import { concat, equals, fromVertices } from './index.js'

test('concat: no paths produces an empty open path', (t) => {
t.true(equals(concat(), fromVertices({ closed: false }, [])))
})

test('concat: empty paths produces an empty open path', (t) => {
t.true(equals(concat(fromVertices({}, []), fromVertices({}, [])), fromVertices({ closed: false }, [])))
})

test('concat: many open paths produces a open path', (t) => {
const p1 = fromVertices({ closed: false }, [[0, 0, 0]])
const p2 = fromVertices({ closed: false }, [[1, 1, 0]])
const p3 = fromVertices({ closed: false }, [[1, 1, 0], [3, 3, 0]])

const result = concat(p1, p2, p3)
t.true(equals(result, fromVertices({}, [[0, 0, 0], [1, 1, 0], [3, 3, 0]])))
t.is(p1.vertices.length, 1)
t.is(p2.vertices.length, 1)
t.is(p3.vertices.length, 2)
})

test('concat: an open path and a closed path produces a closed path', (t) => {
t.true(equals(concat(fromVertices({ closed: false }, [[0, 0, 0]]),
fromVertices({ closed: true }, [[1, 1, 0]])),
fromVertices({ closed: true }, [[0, 0, 0], [1, 1, 0]])))
})

test('concat: a closed path and an open path throws an error', (t) => {
const p1 = fromVertices({ closed: true }, [[0, 0, 0]])
const p2 = fromVertices({ closed: false }, [[1, 1, 0]])
t.throws(() => concat(p1, p2), { message: 'Cannot concatenate to a closed path; check the 1th path' })
})
4 changes: 4 additions & 0 deletions packages/modeling/src/geometries/path3/create.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Path3 } from './type.d.ts'
import type { Vec3 } from '../../maths/vec3/type.d.ts'

export function create(vertices?: Array<Vec3>): Path3
30 changes: 30 additions & 0 deletions packages/modeling/src/geometries/path3/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as mat4 from '../../maths/mat4/index.js'

/**
* Represents a 3D geometry consisting of a list of ordered vertices.
*
* @typedef {Object} Path3
* @property {Array} vertices - list of ordered vertices
* @property {boolean} isClosed - true if the path is closed where start and end vertices are the same
* @property {Mat4} transforms - transforms to apply to the vertices, see transform()
*
* @example
* {
* vertices: [[0,0,0], [4,0,0], [4,3,0]],
* isClosed: true,
* transforms: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],
* }
*/

/**
* Create an empty, open path.
*
* @returns {Path3} a new path
* @function
* @alias module:modeling/geometries/path3.create
*
* @example
* let pathA = create()
* let pathB = create([[0,0,0], [4,0,0], [4,3,0]])
*/
export const create = (vertices = []) => ({ vertices: vertices, isClosed: false, transforms: mat4.create() })
8 changes: 8 additions & 0 deletions packages/modeling/src/geometries/path3/create.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import test from 'ava'

import { create, equals, fromVertices } from './index.js'

test('create: Creates an empty path', (t) => {
t.true(equals(create(), fromVertices({ closed: false }, [])))
t.true(equals(create([[0, 0, 0], [1, 1, 1]]), fromVertices({ closed: false }, [[0, 0, 0], [1, 1, 1]])))
})
3 changes: 3 additions & 0 deletions packages/modeling/src/geometries/path3/equals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Path3 } from './type.d.ts'

export function equals(a: Path3, b: Path3): boolean
48 changes: 48 additions & 0 deletions packages/modeling/src/geometries/path3/equals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as vec3 from '../../maths/vec3/index.js'

import { toVertices } from './toVertices.js'

/**
* Determine if the given paths are equal.
*
* For closed paths, this includes equality by vertex order rotation.
*
* @param {Path3} a - the first path to compare
* @param {Path3} b - the second path to compare
* @returns {boolean}
* @function
* @alias module:modeling/geometries/path3.equals
*/
export const equals = (a, b) => {
if (a.isClosed !== b.isClosed) {
return false
}
if (a.vertices.length !== b.vertices.length) {
return false
}

const aVertices = toVertices(a)
const bVertices = toVertices(b)

// closed paths might be equal under graph rotation
// so try comparison by rotating across all vertices
const length = aVertices.length
let offset = 0
do {
let unequal = false
for (let i = 0; i < length; i++) {
if (!vec3.equals(aVertices[i], bVertices[(i + offset) % length])) {
unequal = true
break
}
}
if (unequal === false) {
return true
}
// unequal open paths should only be compared once, never rotated
if (!a.isClosed) {
return false
}
} while (++offset < length)
return false
}
38 changes: 38 additions & 0 deletions packages/modeling/src/geometries/path3/equals.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import test from 'ava'

import { equals, fromVertices } from './index.js'

test('equals: two paths with different points are not equal', (t) => {
const p1 = fromVertices({ closed: false }, [[0, 0, 0], [2, 0, 0], [2, 1, 0]])
const p2 = fromVertices({ closed: false }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]])
t.false(equals(p1, p2))

const p3 = fromVertices({ closed: true }, [[2, 0, 0], [2, 1, 0], [0, 1, 0], [1, 0, 0]])
const p4 = fromVertices({ closed: true }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]])
t.false(equals(p3, p4))
})

test('equals: two open paths with the same points are equal', (t) => {
const p1 = fromVertices({ closed: false }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]])
const p2 = fromVertices({ closed: false }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]])
t.true(equals(p1, p2))
})

test('equals: two open paths with the same points rotated are unequal', (t) => {
t.false(equals(fromVertices({ closed: false }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]]),
fromVertices({ closed: false }, [[2, 0, 0], [2, 1, 0], [0, 1, 0], [0, 0, 0]])))
})

test('equals: two closed paths with the same points are equal', (t) => {
t.true(equals(fromVertices({ closed: true }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]]),
fromVertices({ closed: true }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]])))

// rotated
t.true(equals(fromVertices({ closed: true }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]]),
fromVertices({ closed: true }, [[2, 0, 0], [2, 1, 0], [0, 1, 0], [0, 0, 0]])))
})

test('equals: closed path and open path with the same points are unequal', (t) => {
t.false(equals(fromVertices({ closed: true }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]]),
fromVertices({ closed: false }, [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]])))
})
Loading