diff --git a/apps/jscad-web/main.js b/apps/jscad-web/main.js
index e8ec77c..b0e181d 100644
--- a/apps/jscad-web/main.js
+++ b/apps/jscad-web/main.js
@@ -21,6 +21,7 @@ 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
@@ -28,6 +29,7 @@ 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)
@@ -59,7 +61,7 @@ async function resetFileRefs(){
if(sw){
delete sw.fileToRun
await clearFs(sw)
- }
+ }
}
async function initFs() {
@@ -198,7 +200,7 @@ sendCmdAndSpin('init', {
},
}).then(() => {
if (loadDefault) {
- runScript({ script: defaultCode })
+ runScript({ script: defaultCode, smooth: viewState.smoothRender })
}
})
@@ -213,19 +215,19 @@ const paramChangeCallback = async params => {
}
working = true
let result
- try {
- result = await sendCmdAndSpin('runMain', { params })
- } finally {
+ try{
+ result = await sendCmdAndSpin('runMain', { params, smooth: viewState.smoothRender })
+ } finally{
working = false
}
- handlers.entities(result)
- if (lastParams && lastParams != params) paramChangeCallback(lastParams)
+ handlers.entities(result, {smooth: viewState.smoothRender})
+ if(lastParams && lastParams != params) paramChangeCallback(lastParams)
}
const runScript = async ({ script, url = './jscad.model.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)
}
diff --git a/apps/jscad-web/src/viewState.js b/apps/jscad-web/src/viewState.js
index ba54dbe..49b7108 100644
--- a/apps/jscad-web/src/viewState.js
+++ b/apps/jscad-web/src/viewState.js
@@ -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) {
@@ -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))
}
@@ -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
@@ -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) {
@@ -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] }
}
@@ -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(){}
}
diff --git a/apps/jscad-web/static/index.html b/apps/jscad-web/static/index.html
index 8644dc0..1aa9471 100644
--- a/apps/jscad-web/static/index.html
+++ b/apps/jscad-web/static/index.html
@@ -82,6 +82,7 @@
Options
+
Documentation
diff --git a/packages/format-threejs/index.js b/packages/format-threejs/index.js
index 3a74d1b..8418cf9 100644
--- a/packages/format-threejs/index.js
+++ b/packages/format-threejs/index.js
@@ -1,4 +1,4 @@
-export function CommonToThree ({
+export function CommonToThree({
MeshPhongMaterial,
LineBasicMaterial,
BufferGeometry,
@@ -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'
@@ -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)
@@ -54,10 +53,11 @@ 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
@@ -65,15 +65,11 @@ export function CommonToThree ({
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
@@ -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
+}
diff --git a/packages/render-threejs/index.js b/packages/render-threejs/index.js
index d3af7a4..d66513e 100644
--- a/packages/render-threejs/index.js
+++ b/packages/render-threejs/index.js
@@ -24,6 +24,7 @@ export function RenderThreejs({
let _camera
let controls
let renderer
+ let smooth
const SHADOW = false
const shouldRender = Date.now()
const lastRender = true
@@ -44,9 +45,10 @@ export function RenderThreejs({
Line,
LineSegments,
Color,
+ Vector3,
})
- const startRenderer = ({ canvas, cameraPosition = [180, -180, 220], cameraTarget = [0, 0, 0], bg = [1, 1, 1] }) => {
+ const startRenderer = ({ canvas, cameraPosition = [180, -180, 220], cameraTarget = [0, 0, 0], bg = [1, 1, 1], lightPosition}) => {
_camera = new PerspectiveCamera(45, 1, 1, 50000)
_camera.up.set(0, 0, 1)
_camera.position.set(...cameraPosition)
@@ -56,16 +58,15 @@ export function RenderThreejs({
window.updateView = updateView
_scene = new Scene()
-
- const ambientLight = new AmbientLight(0xeeeeee, 0.2)
+ lightPosition = null
+ const ambientLight = new AmbientLight(0xeeeeee, lightPosition ? 0.2 : 0.5)
_scene.add(ambientLight)
const hemiLight = new HemisphereLight(0xeeeedd, 0x333333, 0.5)
hemiLight.position.set(0, 0, 2000)
- _scene.add(hemiLight)
+ if(lightPosition) _scene.add(hemiLight)
const directionalLight = new DirectionalLight(0xeeeef4, 0.7)
- directionalLight.position.set(0, -200, 100)
directionalLight.castShadow = SHADOW
if (SHADOW) {
directionalLight.shadow.camera.top = 180
@@ -73,8 +74,15 @@ export function RenderThreejs({
directionalLight.shadow.camera.left = -120
directionalLight.shadow.camera.right = 120
}
- _scene.add(directionalLight)
-
+ if(lightPosition){
+ directionalLight.position.set(...lightPosition)
+ _scene.add(directionalLight)
+ }else{
+ // set pos relative to camera
+ directionalLight.position.set(50,0,100)
+ _camera.add(directionalLight)
+ _scene.add(_camera)
+ }
setBg(bg)
renderer = new WebGLRenderer({ antialias: true, preserveDrawingBuffer: true, canvas })
@@ -93,6 +101,10 @@ export function RenderThreejs({
updateView()
}
+ function setSmooth(v){
+ smooth = v
+ }
+
function setMeshColor(bg = [1, 1, 1]) {
meshColor = new Color(...bg)
csgConvert.setDefColor(bg)
@@ -186,7 +198,8 @@ export function RenderThreejs({
}
}
- function setScene(scene) {
+ function setScene(scene,{smooth}={}) {
+ console.warn('options', {smooth})
groups.forEach(group => {
_scene.remove(group)
})
@@ -198,7 +211,7 @@ export function RenderThreejs({
const group = new Group()
groups.push(group)
item.items.forEach(obj => {
- const obj3d = csgConvert(obj, scene, meshColor)
+ const obj3d = csgConvert(obj, { smooth, scene, meshColor})
if (obj3d) {
entities.push(obj3d)
group.add(obj3d)