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

feat(modeling): preserve color for offset and extrude #1275

Merged
merged 3 commits into from
Aug 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 packages/modeling/src/geometries/geom2/reverse.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ import { clone } from './clone.js'
export const reverse = (geometry) => {
const reversed = clone(geometry)
reversed.outlines = reversed.outlines.map((outline) => outline.slice().reverse())
platypii marked this conversation as resolved.
Show resolved Hide resolved
if (geometry.color) reversed.color = geometry.color
return reversed
}
9 changes: 9 additions & 0 deletions packages/modeling/src/geometries/geom2/reverse.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import test from 'ava'

import { colorize } from '../../colors/index.js'

import { create, reverse, toPoints } from './index.js'

import { comparePoints, compareVectors } from '../../../test/helpers/index.js'
Expand Down Expand Up @@ -29,3 +31,10 @@ test('reverse: does not modify input geometry', (t) => {
t.true(comparePoints(toPoints(geometry), forward))
t.true(comparePoints(toPoints(another), backward))
})

test('reverse: preserves color', (t) => {
const points = [[0, 0], [1, 0], [0, 1]]
const geometry = colorize([1, 0, 0], create([points]))
const reversed = reverse(geometry)
t.deepEqual(reversed.color, [1, 0, 0, 1])
})
13 changes: 13 additions & 0 deletions packages/modeling/src/operations/extrusions/extrudeLinear.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { comparePolygonsAsPoints } from '../../../test/helpers/index.js'

import { TAU } from '../../maths/constants.js'

import { colorize } from '../../colors/index.js'

import { geom2, geom3, path2 } from '../../geometries/index.js'

import { measureVolume } from '../../measurements/index.js'
Expand Down Expand Up @@ -37,6 +39,17 @@ test('extrudeLinear (defaults)', (t) => {
t.true(comparePolygonsAsPoints(pts, exp))
})

test('extrudeLinear: preserves color', (t) => {
const red = colorize([1, 0, 0], square())
const extruded = extrudeLinear({ }, red)
t.deepEqual(extruded.color, [1, 0, 0, 1])
platypii marked this conversation as resolved.
Show resolved Hide resolved

// one red, one blue
const out = extrudeLinear({ }, [red, square()])
t.deepEqual(out[0].color, [1, 0, 0, 1])
t.is(out[1].color, undefined)
})

test('extrudeLinear (no twist)', (t) => {
const geometry2 = square({ size: 10 })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,7 @@ export const extrudeLinearGeom2 = (options, geometry) => {
repair,
callback: createTwist
}
return extrudeFromSlices(options, baseSlice)
const output = extrudeFromSlices(options, baseSlice)
if (geometry.color) output.color = geometry.color
return output
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export const extrudeLinearPath2 = (options, geometry) => {
// Convert path2 to geom2
const points = path2.toPoints(geometry)
const geometry2 = geom2.create([points])
if (geometry.color) geometry2.color = geometry.color
return extrudeLinearGeom2(options, geometry2)
}
13 changes: 8 additions & 5 deletions packages/modeling/src/operations/extrusions/extrudeRotate.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const extrudeRotate = (options, geometry) => {
// convert geometry to an array of sides, easier to deal with
let shapeSides = geom2.toSides(geometry)
if (shapeSides.length === 0) throw new Error('the given geometry cannot be empty')
platypii marked this conversation as resolved.
Show resolved Hide resolved
let sliceGeometry = geometry
z3dev marked this conversation as resolved.
Show resolved Hide resolved

// determine if the extrusion can be computed in the first place
// ie all the points have to be either x > 0 or x < 0
Expand Down Expand Up @@ -87,8 +88,8 @@ export const extrudeRotate = (options, geometry) => {
return [point0, point1]
})
// recreate the geometry from the (-) capped points
geometry = geom2.reverse(geom2.fromSides(shapeSides))
geometry = mirrorX(geometry)
sliceGeometry = geom2.reverse(geom2.fromSides(shapeSides))
sliceGeometry = mirrorX(sliceGeometry)
} else if (pointsWithPositiveX.length >= pointsWithNegativeX.length) {
shapeSides = shapeSides.map((side) => {
let point0 = side[0]
Expand All @@ -98,13 +99,13 @@ export const extrudeRotate = (options, geometry) => {
return [point0, point1]
})
// recreate the geometry from the (+) capped points
geometry = geom2.fromSides(shapeSides)
sliceGeometry = geom2.fromSides(shapeSides)
}
}

const rotationPerSlice = totalRotation / segments
const isCapped = Math.abs(totalRotation) < TAU
let baseSlice = slice.fromGeom2(geometry)
let baseSlice = slice.fromGeom2(sliceGeometry)
baseSlice = slice.reverse(baseSlice)

const matrix = mat4.create()
Expand All @@ -126,5 +127,7 @@ export const extrudeRotate = (options, geometry) => {
close: !isCapped,
callback: createSlice
}
return extrudeFromSlices(options, baseSlice)
const output = extrudeFromSlices(options, baseSlice)
if (geometry.color) output.color = geometry.color
return output
}
10 changes: 10 additions & 0 deletions packages/modeling/src/operations/extrusions/extrudeRotate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { comparePoints, comparePolygonsAsPoints } from '../../../test/helpers/in

import { TAU } from '../../maths/constants.js'

import { colorize } from '../../colors/index.js'

import { geom2, geom3 } from '../../geometries/index.js'

import { measureVolume } from '../../measurements/index.js'

import { square } from '../../primitives/index.js'

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

test('extrudeRotate: (defaults) extruding of a geom2 produces an expected geom3', (t) => {
Expand All @@ -20,6 +24,12 @@ test('extrudeRotate: (defaults) extruding of a geom2 produces an expected geom3'
t.is(pts.length, 96)
})

test('extrudeRotate: preserves color', (t) => {
const red = colorize([1, 0, 0], square())
const extruded = extrudeRotate({ }, red)
t.deepEqual(extruded.color, [1, 0, 0, 1])
})

test('extrudeRotate: (angle) extruding of a geom2 produces an expected geom3', (t) => {
const geometry2 = geom2.create([[[10, 8], [10, -8], [26, -8], [26, 8]]])

Expand Down
4 changes: 3 additions & 1 deletion packages/modeling/src/operations/extrusions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ const projectGeom3 = (options, geometry) => {
return geom2.create([cloned])
})

return unionGeom2(projGeoms)
const output = unionGeom2(projGeoms)
if (geometry.color) output.color = geometry.color
return output
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/modeling/src/operations/extrusions/project.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import test from 'ava'

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

import { colorize } from '../../colors/index.js'

import { geom2, geom3 } from '../../geometries/index.js'

import { measureArea } from '../../measurements/index.js'
Expand Down Expand Up @@ -139,3 +141,9 @@ test('project torus (martinez issue #155)', (t) => {
t.notThrows(() => geom2.validate(result))
t.is(measureArea(result), 21.15545050788201)
})

test('project: preserves color', (t) => {
const red = colorize([1, 0, 0], cube())
const result = project({ }, red)
t.deepEqual(result.color, [1, 0, 0, 1])
})
5 changes: 4 additions & 1 deletion packages/modeling/src/operations/offsets/offsetGeom2.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ export const offsetGeom2 = (options, geometry) => {
}
return offsetFromPoints(options, outline)
})
// TODO: union outlines that expanded into each other

// create a composite geometry from the new outlines
return geom2.create(newOutlines)
const output = geom2.create(newOutlines)
if (geometry.color) output.color = geometry.color
return output
}
19 changes: 11 additions & 8 deletions packages/modeling/src/operations/offsets/offsetGeom2.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import test from 'ava'

import { colorize } from '../../colors/index.js'
import { geom2 } from '../../geometries/index.js'

import { measureArea } from '../../measurements/index.js'

import { roundedRectangle, square } from '../../primitives/index.js'

import { offset } from './index.js'
Expand All @@ -12,12 +11,10 @@ import { comparePoints } from '../../../test/helpers/index.js'

test('offset: offset an empty geom2', (t) => {
const empty = geom2.create()
const obs = offset({ delta: 1 }, empty)
const pts = geom2.toPoints(obs)
const exp = []
t.notThrows(() => geom2.validate(obs))
t.is(measureArea(obs), 0)
t.true(comparePoints(pts, exp))
const result = offset({ delta: 1 }, empty)
t.notThrows(() => geom2.validate(result))
t.is(measureArea(result), 0)
t.is(geom2.toPoints(result).length, 0)
})

test('offset: offset option validation', (t) => {
Expand All @@ -38,6 +35,12 @@ test('offset: offset option validation', (t) => {
t.throws(() => offset({ corners: 'fluffy' }, empty), { message: 'corners must be "edge", "chamfer", or "round"' })
})

test('offset: offset geom2 preserves color', (t) => {
const geometry = colorize([1, 0, 0], square({ }))
const result = offset({ }, geometry)
t.deepEqual(result.color, [1, 0, 0, 1])
})

test('offset: offset of a geom2 produces expected changes to points', (t) => {
const geometry = square({ size: 16 })

Expand Down
7 changes: 3 additions & 4 deletions packages/modeling/src/operations/offsets/offsetGeom3.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ export const offsetGeom3 = (options, geometry) => {
throw new Error('corners must be "round" for 3D geometries')
}

const polygons = geom3.toPolygons(geometry)
if (polygons.length === 0) throw new Error('the given geometry cannot be empty')

options = { delta, corners, segments }
const expanded = offsetShell(options, geometry)
return union(geometry, expanded)
const output = union(geometry, expanded)
if (geometry.color) output.color = geometry.color
return output
}
20 changes: 18 additions & 2 deletions packages/modeling/src/operations/offsets/offsetGeom3.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,28 @@ import test from 'ava'

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

import { colorize } from '../../colors/index.js'
import { geom3, poly3 } from '../../geometries/index.js'

import { sphere } from '../../primitives/index.js'
import { measureVolume } from '../../measurements/index.js'
import { cube, sphere } from '../../primitives/index.js'

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

test('offset: offset empty geom3', (t) => {
const geometry = geom3.create()
const result = offset({ }, geometry)
t.notThrows(() => geom3.validate(result))
t.is(measureVolume(result), 0)
t.is(geom3.toPolygons(result).length, 0)
t.is(geom3.toPoints(result).length, 0)
})

test('offset: offset geom3 preserves color', (t) => {
const geometry = colorize([1, 0, 0], cube({ }))
const result = offset({ }, geometry)
t.deepEqual(result.color, [1, 0, 0, 1])
})

test('offset: offset of a geom3 produces expected changes to polygons', (t) => {
const polygonsAsPoints = [
[[-5, -5, -5], [-5, -5, 15], [-5, 15, 15], [-5, 15, -5]],
Expand Down
14 changes: 7 additions & 7 deletions packages/modeling/src/operations/offsets/offsetPath2.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,17 @@ export const offsetPath2 = (options, geometry) => {

const closed = geometry.isClosed
const points = path2.toPoints(geometry)
if (points.length === 0) throw new Error('the given geometry cannot be empty')
if (points.length === 0) return geometry
z3dev marked this conversation as resolved.
Show resolved Hide resolved

const paths = {
points: points,
points,
external: offsetFromPoints({ delta, corners, segments, closed }, points),
internal: offsetFromPoints({ delta: -delta, corners, segments, closed }, points)
}

if (geometry.isClosed) {
return createGeometryFromClosedOffsets(paths)
} else {
return createGeometryFromExpandedOpenPath(paths, segments, corners, delta)
}
const output = geometry.isClosed ?
createGeometryFromClosedOffsets(paths) :
createGeometryFromExpandedOpenPath(paths, segments, corners, delta)
z3dev marked this conversation as resolved.
Show resolved Hide resolved
if (geometry.color) output.color = geometry.color
return output
}
17 changes: 16 additions & 1 deletion packages/modeling/src/operations/offsets/offsetPath2.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@ import test from 'ava'

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

import { colorize } from '../../colors/index.js'
import { geom2, geom3, path2 } from '../../geometries/index.js'
import { measureBoundingBox } from '../../measurements/index.js'
import { measureArea, measureBoundingBox } from '../../measurements/index.js'
import { area } from '../../maths/utils/index.js'
import { TAU } from '../../maths/constants.js'
import { sphere, square } from '../../primitives/index.js'

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

test('offset: offset empty path2', (t) => {
const geometry = path2.create()
const result = offset({ }, geometry)
t.notThrows(() => path2.validate(result))
t.is(measureArea(result), 0)
t.is(path2.toPoints(result).length, 0)
})

test('offset: offset path2 preserves color', (t) => {
const geometry = colorize([1, 0, 0], path2.create())
const result = offset({ }, geometry)
t.deepEqual(result.color, [1, 0, 0, 1])
})

test('offset: edge-expanding a straight line produces rectangle', (t) => {
const points = [[0, 0], [0, 10]]
const linePath2 = path2.fromPoints({ closed: false }, points)
Expand Down
4 changes: 3 additions & 1 deletion packages/modeling/src/operations/offsets/offsetShell.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export const offsetShell = (options, geometry) => {
}
const { delta, segments } = Object.assign({ }, defaults, options)

const polygons = geom3.toPolygons(geometry)
if (polygons.length === 0) return geometry
platypii marked this conversation as resolved.
Show resolved Hide resolved

let result = geom3.create()
const vertices2planes = new Map() // {vertex: [vertex, [plane, ...]]}
const edges2planes = new Map() // {edge: [[vertex, vertex], [plane, ...]]}
Expand All @@ -82,7 +85,6 @@ export const offsetShell = (options, geometry) => {
// - extruded the polygon, and add to the composite result
// - add the plane to the unique vertex map
// - add the plane to the unique edge map
const polygons = geom3.toPolygons(geometry)
polygons.forEach((polygon, index) => {
const extrudeVector = vec3.scale(vec3.create(), poly3.plane(polygon), 2 * delta)
const translatedPolygon = poly3.transform(mat4.fromTranslation(mat4.create(), vec3.scale(vec3.create(), extrudeVector, -0.5)), polygon)
Expand Down