Skip to content

Commit

Permalink
Add type information to jscad-web (#128)
Browse files Browse the repository at this point in the history
I have activated strict ts typechecks in vscode. This resulted in a lot
of warnings. I have used these warnings to add missing type information
to a lot of places in the code. The project is still far away from full
type safety but its (hopefully) a step in the right direction.

I know this PR is pretty large, a bit all over the place and hard to
review. Sorry for that. Please contact me for questions, critique or
anything else.
  • Loading branch information
Kaladum authored Nov 20, 2024
1 parent 2d0c630 commit 30e3e71
Show file tree
Hide file tree
Showing 25 changed files with 702 additions and 337 deletions.
269 changes: 140 additions & 129 deletions apps/jscad-web/main.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/jscad-web/src/addV1Shim.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @param {string } script
*/
export function addV1Shim(script) {
if (script.includes('JS V1 SHIM HEADER')) return script

Expand Down
54 changes: 27 additions & 27 deletions apps/jscad-web/src/animRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,68 @@
/** @typedef {import('@jscadui/worker').JscadWorker} JscadWorker*/


export class AnimRunner{
export class AnimRunner {
/**
*
* @param {JscadWorker} worker
*/
constructor(worker, options={}){
constructor(worker, options = {}) {
/** @type {JscadWorker} */
this.worker = worker
this.options = options
}
pause(){

pause() {
this.shouldPause = true
}

isRunning(){
isRunning() {
return this.running
}

async start(def, value, params){
async start(def, value, params) {
this.running = true
this.shouldPause = false

let {fps, min=0, max, loop, name} = def
if(params.fps) fps = params.fps
let step = 1/fps
let { fps, min = 0, max, loop, name } = def
if (params.fps) fps = params.fps
let step = 1 / fps
let minMaxDelta = max - min
let fpsMs = 1000 / fps - 1
value = parseFloat(value) + step

let lastTime, now, delta, resp, paramValues, times
let startTime = lastTime = now = Date.now()
let t = value
let i=1;
let i = 1;
let dir = loop == 'reverse' ? 1 : 0


while(!this.shouldPause){
if(t>max){
while( t> max) t -= minMaxDelta
if(loop == 'reverse'){
while (!this.shouldPause) {
if (t > max) {
while (t > max) t -= minMaxDelta

if (loop == 'reverse') {
dir *= -1
} else if(loop != 'restart'){
} else if (loop != 'restart') {
// end animation
break
}
}

times = {[name]: (dir == 1) ? t : max - t}
paramValues = {...params, ...times}
resp = await this.worker.jscadMain({params:paramValues, skipLog:true})
if(this.shouldPause) break
times = { [name]: (dir == 1) ? t : max - t }
paramValues = { ...params, ...times }
resp = await this.worker.jscadMain({ params: paramValues, skipLog: true })
if (this.shouldPause) break

now = Date.now()
delta = now - lastTime
if(delta < fpsMs){
if (delta < fpsMs) {
await waitTime(fpsMs - delta - 1)
if(this.shouldPause){
console.log('Animation stopped between generating frame for '+name+'='+times[name]+' and rendering it. Discarding the result.')
if (this.shouldPause) {
console.log('Animation stopped between generating frame for ' + name + '=' + times[name] + ' and rendering it. Discarding the result.')
break
}
}
}
lastTime = Date.now()
t += step
Expand All @@ -73,8 +73,8 @@ export class AnimRunner{
}
}

async function waitTime(ms){
return new Promise(resolve=>{
setTimeout(resolve,ms)
async function waitTime(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
25 changes: 14 additions & 11 deletions apps/jscad-web/src/drawer.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
let isMouseDown = false
let isDragging = false
let dragStartX
let dragStartWidth
let dragStartTime
let dragStartX = 0
let dragStartWidth = 0
let dragStartTime = 0

// Initialize drawer action
// Initial open/closed state is in index.html to prevent flash of content
export const init = () => {
const editor = document.getElementById('editor')
const toggle = document.getElementById('editor-toggle')
const editor = /** @type {HTMLElement} */ (document.getElementById('editor'))
const toggle = /** @type {HTMLElement} */ (document.getElementById('editor-toggle'))

// Set editor width and handle open/closed state
/**
* Set editor width and handle open/closed state
* @param {number} w
*/
const setEditorWidth = (w) => {
if (w > 0) {
editor.style.width = `${w}px`
Expand All @@ -24,9 +27,9 @@ export const init = () => {
if (!isDragging) {
editor.classList.add('transition') // animate
const isClosed = editor.classList.contains('closed')
localStorage.setItem('editor.closed', !isClosed)
localStorage.setItem('editor.closed', String(!isClosed))
if (isClosed) {
setEditorWidth(localStorage.getItem('editor.width') || 400)
setEditorWidth(parseInt(localStorage.getItem('editor.width') ?? "400"))
} else {
setEditorWidth(0)
}
Expand Down Expand Up @@ -65,10 +68,10 @@ export const init = () => {
const width = editor.offsetWidth
// Minimum width, otherwise snap to closed
if (width > 50) {
localStorage.setItem('editor.width', width)
localStorage.setItem('editor.closed', false)
localStorage.setItem('editor.width', String(width))
localStorage.setItem('editor.closed', "false")
} else {
localStorage.setItem('editor.closed', true)
localStorage.setItem('editor.closed', "true")
editor.classList.add('transition') // snap closed
setEditorWidth(0)
}
Expand Down
64 changes: 54 additions & 10 deletions apps/jscad-web/src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,43 @@ import { readAsText } from '@jscadui/fs-provider'

import * as drawer from './drawer.js'

/**
* @typedef {import('@jscadui/fs-provider').FSFileEntry} FSFileEntry
*
* @callback CompileFn
* @param {string} code
* @param {string} path
*
* @callback SaveFn
* @param {string} code
* @param {string} path
*
* @callback GetFileFn
* @param {string} path
* @returns {Promise<FSFileEntry | undefined>}
*/

/** @type {EditorView} */
let view

/** @type {CompileFn} */
let compileFn
/** @type {SaveFn} */
let saveFn
/** @type {GetFileFn} */
let getFileFn

// file selector
let currentFile = '/index.js'
/** @type {HTMLElement} */
let editorNav
/** @type {HTMLElement} */
let editorFile

/**
* @param {string} code
* @param {string} path
*/
const compile = (code, path) => {
if (compileFn) {
compileFn(code, path)
Expand All @@ -25,17 +51,27 @@ const compile = (code, path) => {
}
}

/**
* @param {string} code
* @param {string} path
*/
const save = (code, path) => {
compileFn(code, path)
saveFn(code, path)
}

export const runScript = ()=>compile(view.state.doc.toString(), currentFile)
export const runScript = () => compile(view.state.doc.toString(), currentFile)

/**
* @param {string} defaultCode
* @param {CompileFn} fn
* @param {SaveFn} _saveFn
* @param {GetFileFn} _getFileFn
*/
export const init = (defaultCode, fn, _saveFn, _getFileFn) => {
// by calling document.getElementById here instead outside of init we allow the flow
// where javascript is included in the page before the tempalte is loaded into the DOM
// it was causing issue to users trying to replicate the app in Vue, and would likely some others too
// where javascript is included in the page before the template is loaded into the DOM
// it was causing issue to users trying to replicate the app in Vue, and would likely some others too
editorNav = document.getElementById('editor-nav')
editorFile = document.getElementById('editor-file')

Expand Down Expand Up @@ -89,34 +125,42 @@ export const init = (defaultCode, fn, _saveFn, _getFileFn) => {
})
}

/** @returns {string} */
export const getSource = () => view.state.doc.toString()

/**
* @param {string} source
* @param {string} path
*/
export const setSource = (source, path = '/index.js') => {
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: source } })
currentFile = path
}

export async function filesChanged(files){
export async function filesChanged(files) {
let file
for(let i=0; i<files.length; i++){
let path = files[i]
if(path == currentFile){
for (let i = 0; i < files.length; i++) {
let path = files[i]
if (path == currentFile) {
file = await getFileFn(path)
readSource(file, currentFile)
}else if(path.name === currentFile){
} else if (path.name === currentFile) {
setSource(await readAsText(path), currentFile)
}
}
}

async function readSource(file, currentFile){
async function readSource(file, currentFile) {
setSource(await readAsText(file), currentFile)
}

let editorFilesArr = []

export const getEditorFiles = ()=>editorFilesArr
export const getEditorFiles = () => editorFilesArr

/**
* @param {FSFileEntry[]} files
*/
export const setFiles = (files) => {
const editorFiles = document.getElementById('editor-files')
editorFilesArr = files
Expand Down
13 changes: 11 additions & 2 deletions apps/jscad-web/src/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import { RenderThreejs } from '@jscadui/render-threejs'
export const init = async () => {
await addScript('build/bundle.threejs.js')
const JscadThreeViewer = RenderThreejs(THREE)
const el = document.getElementById('viewer')
const el = /** @type {HTMLDivElement} */ (document.getElementById('viewer'))
const viewer = JscadThreeViewer(el)

observeResize(el, evt => viewer.resize(evt.contentRect))

return viewer
}

/**
* @param {HTMLElement} el
* @param {(entry:ResizeObserverEntry)=>void} listener
*/
const observeResize = (el, listener) => {
// ResizeObserver is better than window resize as it can be used on any element
// this is a short/compact/simple implementation that uses a new ResizeObserver each time.
Expand All @@ -24,9 +28,14 @@ const observeResize = (el, listener) => {
resizeObserver.observe(el)
}

/**
* @param {string} source
* @param {boolean} [module]
* @returns {Promise<void>}
*/
const addScript = async (source, module = false) => {
return new Promise((resolve, reject) => {
var tag = document.createElement('script')
const tag = document.createElement('script')
tag.type = module ? 'module' : 'text/javascript'
tag.src = source
tag.onload = () => resolve()
Expand Down
23 changes: 21 additions & 2 deletions apps/jscad-web/src/stacktrace.js → apps/jscad-web/src/error.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
const errorBar = document.getElementById('error-bar')
const errorName = document.getElementById('error-name')
const errorMessage = document.getElementById('error-message')

/**
* @param {unknown} error
*/
export const setError = error => {
if (error) {
const name = (error.name || 'Error') + ': '
errorName.innerText = name
const message = formatStacktrace(error)
errorMessage.innerText = message
errorBar.classList.add('visible')
} else {
errorBar.classList.remove('visible')
}
}

/**
* Extracts the stacktrace for an error thrown from inside an eval function.
* Returns the stacktrace as a string for just the code running inside eval.
*
* @param {Error} error
* @returns {string} - stacktrace for code inside eval
*/
export const formatStacktrace = (error) => {
const formatStacktrace = (error) => {
// error.stack is not standard but works on chrome and firefox
const stack = error.stack
if (!stack) return error.toString()
Expand All @@ -21,6 +40,6 @@ export const formatStacktrace = (error) => {
.split('\n')
.filter(line => !line.includes('bundle.worker.js'))

if(!stack.includes(error.message)) cleaned.unshift(error.message)
if (!stack.includes(error.message)) cleaned.unshift(error.message)
return cleaned.join('\n')
}
Loading

0 comments on commit 30e3e71

Please sign in to comment.