Skip to content

Commit

Permalink
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 59 deletions.
10 changes: 6 additions & 4 deletions apps/jscad-web/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import * as remote from './src/remote.js'
import { formatStacktrace } from './src/stacktrace.js'
import { ViewState } from './src/viewState.js'
import * as welcome from './src/welcome.js'
import { runMain } from '../../packages/worker/worker.js'

export const byId = id => document.getElementById(id)
const appBase = document.baseURI
let currentBase = appBase
const toUrl = path => new URL(path, appBase).toString()

const viewState = new ViewState()
viewState.onRequireReRender = ()=>paramChangeCallback(lastParams)

const gizmo = (window.gizmo = new Gizmo())
byId('overlay').parentNode.appendChild(gizmo)
Expand Down Expand Up @@ -173,7 +175,7 @@ sendCmdAndSpin('init', {
},
}).then(() => {
if (loadDefault) {
runScript({ script: defaultCode })
runScript({ script: defaultCode, smooth: viewState.smoothRender })
}
})

Expand All @@ -189,18 +191,18 @@ const paramChangeCallback = async params => {
working = true
let result
try{
result = await sendCmdAndSpin('runMain', { params })
result = await sendCmdAndSpin('runMain', { params, smooth: viewState.smoothRender })
} finally{
working = false
}
handlers.entities(result)
handlers.entities(result, {smooth: viewState.smoothRender})
if(lastParams && lastParams != params) paramChangeCallback(lastParams)
}

const runScript = async ({ script, url = './index.js', base = currentBase, root }) => {
currentBase = base
loadDefault = false // don't load default model if something else was loaded
const result = await sendCmdAndSpin('runScript', { script, url, base, root })
const result = await sendCmdAndSpin('runScript', { script, url, base, root, smooth: viewState.smoothRender })
genParams({ target: byId('paramsDiv'), params: result.def || {}, callback: paramChangeCallback })
handlers.entities(result)
}
Expand Down
17 changes: 16 additions & 1 deletion apps/jscad-web/src/viewState.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class ViewState {
const darkMode = byId('dark-mode')
const showAxis = byId('show-axis')
const showGrid = byId('show-grid')
const smoothRender = byId('smooth-render')
darkMode.addEventListener('change', () => {
this.themeName = darkMode.checked ? 'dark' : 'light'
if (darkMode.checked) {
Expand All @@ -25,6 +26,9 @@ export class ViewState {
}
this.setTheme(this.themeName)
})
smoothRender.addEventListener('change', () => {
this.setSmoothRender(smoothRender.checked)
})
showAxis.addEventListener('change', () => this.setAxes(showAxis.checked))
showGrid.addEventListener('change', () => this.setGrid(showGrid.checked))
}
Expand All @@ -41,6 +45,12 @@ export class ViewState {
this.saveState()
}

setSmoothRender(smoothRender, fireEvent = true) {
this.smoothRender = smoothRender
this.saveState()
if(fireEvent) this.onRequireReRender()
}

setTheme(themeName) {
if (!themes[themeName]) throw new Error(`unknown theme ${themeName}`)
this.themeName = themeName
Expand Down Expand Up @@ -86,7 +96,7 @@ export class ViewState {
if (grid) items.push({ id: 'grid', items: grid })
if (model) items.push({ id: 'model', items: model })

this.viewer?.setScene({ items })
this.viewer?.setScene({ items }, {smooth:this.smoothRender})
}

setEngine(viewer) {
Expand All @@ -108,6 +118,8 @@ export class ViewState {
byId('show-axis').checked = this.showAxis
this.showGrid = localStorage.getItem('engine.showGrid') !== 'false'
byId('show-grid').checked = this.showGrid
this.smoothRender = !!localStorage.getItem('engine.smoothRender')
byId('smooth-render').checked = this.smoothRender
const cameraLocation = localStorage.getItem('camera.location')
this.camera = cameraLocation ? JSON.parse(cameraLocation) : { position: [180, -180, 220] }
}
Expand All @@ -116,5 +128,8 @@ export class ViewState {
localStorage.setItem('engine.theme', this.themeName)
localStorage.setItem('engine.showAxis', this.showAxis)
localStorage.setItem('engine.showGrid', this.showGrid)
localStorage.setItem('engine.smoothRender', this.smoothRender)
}

onRequireReRender(){}
}
1 change: 1 addition & 0 deletions apps/jscad-web/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ <h2>Options</h2>
<li><label for="dark-mode">Dark Mode</label><input type="checkbox" id="dark-mode"></li>
<li><label for="show-axis">Show Axis</label><input type="checkbox" id="show-axis" checked></li>
<li><label for="show-grid">Show Grid</label><input type="checkbox" id="show-grid" checked></li>
<li><label for="smooth-render">Smooth Render</label><input type="checkbox" id="smooth-render" checked></li>
</ul>

<h2>Documentation</h2>
Expand Down
186 changes: 141 additions & 45 deletions packages/format-threejs/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function CommonToThree ({
export function CommonToThree({
MeshPhongMaterial,
LineBasicMaterial,
BufferGeometry,
Expand All @@ -7,26 +7,25 @@ export function CommonToThree ({
InstancedMesh,
Line,
LineSegments,
Color
Color,
Vector3,
}) {

const flatShading = true
const flatShading = false
const materials = {
mesh: {
def: new MeshPhongMaterial({ color: 0x0084d1, flatShading }),
make: params => new MeshPhongMaterial({ flatShading, ...params })
make: params => new MeshPhongMaterial({ flatShading, ...params }),
},
line: {
def: new LineBasicMaterial({ color: 0x0000ff }),
make: params =>
new LineBasicMaterial(params)
make: params => new LineBasicMaterial(params),
},
lines: null
lines: null,
}
materials.lines = materials.line
materials.instance = materials.mesh// todo support instances for lines
materials.instance = materials.mesh // todo support instances for lines

function _CSG2Three (obj) {
function _CSG2Three(obj, { smooth = false }) {
const { vertices, indices, normals, color, colors, isTransparent = false, opacity } = obj
let { transforms } = obj
const objType = obj.type || 'mesh'
Expand All @@ -43,7 +42,7 @@ export function CommonToThree ({
const opts = {
vertexColors: !!colors,
opacity: c[3] === undefined ? 1 : c[3],
transparent: (color && c[3] !== 1 && c[3] !== undefined) || isTransparent
transparent: (color && c[3] !== 1 && c[3] !== undefined) || isTransparent,
}
if (opacity) opts.opacity = opacity
if (!colors) opts.color = _CSG2Three.makeColor(color)
Expand All @@ -54,26 +53,23 @@ export function CommonToThree ({
}
}

const geo = new BufferGeometry()
let geo = new BufferGeometry()
geo.setAttribute('position', new BufferAttribute(vertices, 3))
if (indices) geo.setIndex(new BufferAttribute(indices, 1))
if (normals) geo.setAttribute('normal', new BufferAttribute(normals, 3))
if(smooth) geo = toCreasedNormals({ Vector3, BufferAttribute }, geo, Math.PI / 10)
if (colors) geo.setAttribute('color', new BufferAttribute(colors, isTransparent ? 4 : 3))

let mesh
switch (objType) {
case 'mesh':
mesh = new Mesh(geo, material)
break
case 'instance':
const {list} = obj
mesh = new InstancedMesh(
geo,
materials.mesh.make({ color: 0x0084d1 }),
list.length
)
list.forEach((item, i)=>{
copyTransformToArray(item.transforms, mesh.instanceMatrix.array,i*16)
case 'instance':
const { list } = obj
mesh = new InstancedMesh(geo, materials.mesh.make({ color: 0x0084d1 }), list.length)
list.forEach((item, i) => {
copyTransformToArray(item.transforms, mesh.instanceMatrix.array, i * 16)
})
transforms = null
break
Expand All @@ -90,37 +86,137 @@ export function CommonToThree ({
}

// shortcut for setMatrixAt for InstancedMesh
function copyTransformToArray( te, array = [], offset = 0 ) {

array[ offset ] = te[ 0 ];
array[ offset + 1 ] = te[ 1 ];
array[ offset + 2 ] = te[ 2 ];
array[ offset + 3 ] = te[ 3 ];
function copyTransformToArray(te, array = [], offset = 0) {
array[offset] = te[0]
array[offset + 1] = te[1]
array[offset + 2] = te[2]
array[offset + 3] = te[3]

array[ offset + 4 ] = te[ 4 ];
array[ offset + 5 ] = te[ 5 ];
array[ offset + 6 ] = te[ 6 ];
array[ offset + 7 ] = te[ 7 ];
array[offset + 4] = te[4]
array[offset + 5] = te[5]
array[offset + 6] = te[6]
array[offset + 7] = te[7]

array[ offset + 8 ] = te[ 8 ];
array[ offset + 9 ] = te[ 9 ];
array[ offset + 10 ] = te[ 10 ];
array[ offset + 11 ] = te[ 11 ];
array[offset + 8] = te[8]
array[offset + 9] = te[9]
array[offset + 10] = te[10]
array[offset + 11] = te[11]

array[ offset + 12 ] = te[ 12 ];
array[ offset + 13 ] = te[ 13 ];
array[ offset + 14 ] = te[ 14 ];
array[ offset + 15 ] = te[ 15 ];
array[offset + 12] = te[12]
array[offset + 13] = te[13]
array[offset + 14] = te[14]
array[offset + 15] = te[15]

return array;

}
return array
}

_CSG2Three.makeColor = (c) => new Color(c[0], c[1], c[2])
_CSG2Three.makeColor = c => new Color(c[0], c[1], c[2])
_CSG2Three.materials = materials
_CSG2Three.setDefColor = (c)=>{
materials.mesh.def = new MeshPhongMaterial({color:_CSG2Three.makeColor(c), flatShading})
_CSG2Three.setDefColor = c => {
materials.mesh.def = new MeshPhongMaterial({ color: _CSG2Three.makeColor(c), flatShading })
}

return _CSG2Three
}

/** from threejs examples BufferGeometryUtils
* @param {BufferGeometry} geometry
* @param {number} tolerance
* @return {BufferGeometry>}
*/

/**
* Modifies the supplied geometry if it is non-indexed, otherwise creates a new,
* non-indexed geometry. Returns the geometry with smooth normals everywhere except
* faces that meet at an angle greater than the crease angle.
*
* @param {BufferGeometry} geometry
* @param {number} [creaseAngle]
* @return {BufferGeometry}
*/
function toCreasedNormals({ Vector3, BufferAttribute }, geometry, creaseAngle = Math.PI / 3 /* 60 degrees */) {
const creaseDot = Math.cos(creaseAngle)
const hashMultiplier = (1 + 1e-10) * 1e2

// reusable vectors
const verts = [new Vector3(), new Vector3(), new Vector3()]
const tempVec1 = new Vector3()
const tempVec2 = new Vector3()
const tempNorm = new Vector3()
const tempNorm2 = new Vector3()

// hashes a vector
function hashVertex(v) {
const x = ~~(v.x * hashMultiplier)
const y = ~~(v.y * hashMultiplier)
const z = ~~(v.z * hashMultiplier)
return `${x},${y},${z}`
}

// BufferGeometry.toNonIndexed() warns if the geometry is non-indexed
// and returns the original geometry
const resultGeometry = geometry.index ? geometry.toNonIndexed() : geometry
const posAttr = resultGeometry.attributes.position
const vertexMap = {}

// find all the normals shared by commonly located vertices
for (let i = 0, l = posAttr.count / 3; i < l; i++) {
const i3 = 3 * i
const a = verts[0].fromBufferAttribute(posAttr, i3 + 0)
const b = verts[1].fromBufferAttribute(posAttr, i3 + 1)
const c = verts[2].fromBufferAttribute(posAttr, i3 + 2)

tempVec1.subVectors(c, b)
tempVec2.subVectors(a, b)

// add the normal to the map for all vertices
const normal = new Vector3().crossVectors(tempVec1, tempVec2).normalize()
for (let n = 0; n < 3; n++) {
const vert = verts[n]
const hash = hashVertex(vert)
if (!(hash in vertexMap)) {
vertexMap[hash] = []
}

vertexMap[hash].push(normal)
}
}

// average normals from all vertices that share a common location if they are within the
// provided crease threshold
const normalArray = new Float32Array(posAttr.count * 3)
const normAttr = new BufferAttribute(normalArray, 3, false)
for (let i = 0, l = posAttr.count / 3; i < l; i++) {
// get the face normal for this vertex
const i3 = 3 * i
const a = verts[0].fromBufferAttribute(posAttr, i3 + 0)
const b = verts[1].fromBufferAttribute(posAttr, i3 + 1)
const c = verts[2].fromBufferAttribute(posAttr, i3 + 2)

tempVec1.subVectors(c, b)
tempVec2.subVectors(a, b)

tempNorm.crossVectors(tempVec1, tempVec2).normalize()

// average all normals that meet the threshold and set the normal value
for (let n = 0; n < 3; n++) {
const vert = verts[n]
const hash = hashVertex(vert)
const otherNormals = vertexMap[hash]
tempNorm2.set(0, 0, 0)

for (let k = 0, lk = otherNormals.length; k < lk; k++) {
const otherNorm = otherNormals[k]
if (tempNorm.dot(otherNorm) > creaseDot) {
tempNorm2.add(otherNorm)
}
}

tempNorm2.normalize()
normAttr.setXYZ(i3 + n, tempNorm2.x, tempNorm2.y, tempNorm2.z)
}
}

resultGeometry.setAttribute('normal', normAttr)
return resultGeometry
}
Loading

0 comments on commit fc619ac

Please sign in to comment.