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

feature-save-file #92

Merged
merged 6 commits into from
Jan 29, 2024
Merged
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,6 @@ dist
.turbo
build/**
dist/**
**/.history
**/.history
**/.idea

151 changes: 111 additions & 40 deletions apps/jscad-web/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { addToCache, extractEntries, fileDropped, getFile, registerServiceWorker } from '@jscadui/fs-provider'
import {
addToCache,
clearFs,
extractEntries,
fileDropped,
getFile,
getFileContent,
registerServiceWorker,
} from '@jscadui/fs-provider'
import { Gizmo } from '@jscadui/html-gizmo'
import { OrbitControl } from '@jscadui/orbit'
import { genParams } from '@jscadui/params'
Expand Down Expand Up @@ -47,20 +55,34 @@ ctrl.oninput = state => updateFromCtrl(state)
gizmo.oncam = ({ cam }) => ctrl.animateToCommonCamera(cam)

let sw
async function resetFileRefs(){
editor.setFiles([])
saveMap = {}
if(sw){
delete sw.fileToRun
await clearFs(sw)
}
}

async function initFs() {
const getFileWrapper = (path, sw) => {
const file = getFile(path, sw)
const file = getFileContent(path, sw)
// notify editor of active files
file.then(() => editor.setFiles(sw.filesToCheck))
return file
}
let scope = document.location.pathname
sw = await registerServiceWorker(`bundle.fs-serviceworker.js?prefix=${scope}swfs/`, getFileWrapper, {scope, prefix:scope+'swfs/'})
sw = await registerServiceWorker(`bundle.fs-serviceworker.js?prefix=${scope}swfs/`, getFileWrapper, {
scope,
prefix: scope + 'swfs/',
})
sw.defProjectName = 'jscad'
sw.onfileschange = files => {
sendNotify('clearFileCache', { files })
editor.filesChanged(files)
if (sw.fileToRun) runScript({ url: sw.fileToRun, base: sw.base })
}
sw.getFile = path => getFile(path, sw)
}
const dropModal = byId('dropModal')
const showDrop = show => {
Expand All @@ -70,19 +92,21 @@ const showDrop = show => {
document.body.ondrop = async ev => {
try {
ev.preventDefault()
let files = extractEntries(ev.dataTransfer)
let files = await extractEntries(ev.dataTransfer)
if (!files.length) return {}

await resetFileRefs()
if (!sw) await initFs()
showDrop(false)
sendCmd('clearTempCache', {})
saveMap = {}
const { alias, script } = await fileDropped(sw, files)
projectName = sw.projectName
if (alias.length) {
sendNotify('init', { alias })
}
runScript({ url: sw.fileToRun, base: sw.base })
editor.setSource(script, sw.fileToRun)
let url = sw.fileToRun
runScript({ url, base: sw.base })
editor.setSource(script, url)
editor.setFiles(sw.filesToCheck)
} catch (error) {
setError(error)
Expand Down Expand Up @@ -135,7 +159,7 @@ const exportModel = async (format, extension) => {

const worker = new Worker('./build/bundle.worker.js')
const handlers = {
entities: ({ entities, mainTime, convertTime}) => {
entities: ({ entities, mainTime, convertTime }) => {
if (!(entities instanceof Array)) entities = [entities]
viewState.setModel((model = entities))
console.log('Main execution:', mainTime?.toFixed(2), ', jscad mesh -> gl:', convertTime?.toFixed(2))
Expand All @@ -149,11 +173,11 @@ let jobs = 0
let firstJobTimer
async function sendCmdAndSpin(method, params) {
jobs++
if(jobs === 1){
if (jobs === 1) {
// do not show spinner for fast renders
firstJobTimer = setTimeout(()=>{
firstJobTimer = setTimeout(() => {
spinner.style.display = 'block'
},300)
}, 300)
}
try {
return await sendCmd(method, params)
Expand All @@ -169,7 +193,8 @@ async function sendCmdAndSpin(method, params) {
}

sendCmdAndSpin('init', {
bundles: {// local bundled alias for common libs.
bundles: {
// local bundled alias for common libs.
'@jscad/modeling': toUrl('./build/bundle.jscad_modeling.js'),
'@jscad/io': toUrl('./build/bundle.jscad_io.js'),
},
Expand All @@ -182,9 +207,9 @@ sendCmdAndSpin('init', {
let working
let lastParams
const paramChangeCallback = async params => {
if(!working){
if (!working) {
lastParams = null
}else{
} else {
lastParams = params
return
}
Expand All @@ -199,16 +224,17 @@ const paramChangeCallback = async params => {
if(lastParams && lastParams != params) paramChangeCallback(lastParams)
}

const runScript = async ({ script, url = './index.js', base = currentBase, root }) => {
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, smooth: viewState.smoothRender })
genParams({ target: byId('paramsDiv'), params: result.def || {}, callback: paramChangeCallback })
handlers.entities(result)
}

const loadExample = (source, base=appBase) => {
editor.setSource(source)
const loadExample = async (source, base = appBase) => {
await resetFileRefs()
editor.setSource(source, base)
runScript({ script: source, base })
}

Expand All @@ -217,32 +243,77 @@ engine.init().then(viewer => {
viewState.setEngine(viewer)
})

editor.init(defaultCode, async (script, path) => {
if (sw && sw.fileToRun) {
await addToCache(sw.cache, path, script)
// imported script will be also cached by require/import implementation
// it is expected if multiple files require same file/module that first time it is loaded
// but for others resolved module is returned
// if not cleared by calling clearFileCache, require will not try to reload the file
await sendCmd('clearFileCache', { files: [path] })
if (sw.fileToRun) runScript({ url: sw.fileToRun, base: sw.base })
} else {
runScript({ script })
let saveMap = {}
setInterval(async ()=>{
for(let p in saveMap){
let handle = saveMap[p]
let file = await handle.getFile()
if(file.lastModified > handle.lastMod){
handle.lastMod = file.lastModified
editor.filesChanged([file])
}
}
})
},500)

editor.init(
defaultCode,
async (script, path) => {
if (sw && sw.fileToRun) {
await addToCache(sw.cache, path, script)
// imported script will be also cached by require/import implementation
// it is expected if multiple files require same file/module that first time it is loaded
// but for others resolved module is returned
// if not cleared by calling clearFileCache, require will not try to reload the file
await sendCmd('clearFileCache', { files: [path] })
if (sw.fileToRun) runScript({ url: sw.fileToRun, base: sw.base })
} else {
runScript({ script })
}
},
async (script, path) => {
console.log('save file', path)
let pathArr = path.split('/')
let fileHandle = (await sw?.getFile(path))?.fileHandle
if(!fileHandle) fileHandle = saveMap[path]
if (!fileHandle) {
const opts = {
suggestedName: pathArr[pathArr.length - 1],
excludeAcceptAllOption: true,
types: [
{
description: 'Javascript',
accept: { 'application/javascript': ['.js'] },
},
],
}
fileHandle = await globalThis.showSaveFilePicker?.(opts)
}
if (fileHandle) {
const writable = await fileHandle.createWritable()
await writable.write(script)
await writable.close()
saveMap[path] = fileHandle
fileHandle.lastMod = Date.now()+500
}
},
path=>sw?.getFile(path)
)
menu.init(loadExample)
welcome.init()
remote.init((script, url) => {
// run remote script
editor.setSource(script)
runScript({ script, base:url })
welcome.dismiss()
}, (err) => {
// show remote script error
loadDefault = false
setError(err)
welcome.dismiss()
})
remote.init(
(script, url) => {
// run remote script
editor.setSource(script, url)
runScript({ script, base: url })
welcome.dismiss()
},
err => {
// show remote script error
loadDefault = false
setError(err)
welcome.dismiss()
},
)
exporter.init(exportModel)

try {
Expand Down
53 changes: 41 additions & 12 deletions apps/jscad-web/src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import * as drawer from './drawer.js'
let view

let compileFn
let saveFn
let getFileFn

// file selector
let currentFile = '/index.js'
Expand All @@ -22,14 +24,21 @@ const compile = (code, path) => {
}
}

export const init = (defaultCode, fn) => {
const save = (code, path) => {
compileFn(code, path)
saveFn(code, path)
}

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
editorNav = document.getElementById('editor-nav')
editorFile = document.getElementById('editor-file')

compileFn = fn
saveFn = _saveFn
getFileFn = _getFileFn
// Initialize codemirror
const editorDiv = document.getElementById('editor-container')
view = new EditorView({
Expand All @@ -44,15 +53,15 @@ export const init = (defaultCode, fn) => {
},
{
key: 'Mod-s',
run: () => compile(view.state.doc.toString(), currentFile),
run: () => save(view.state.doc.toString(), currentFile),
preventDefault: true,
},
...defaultKeymap,
]),
],
parent: editorDiv,
})
setSource(defaultCode)
setSource(defaultCode, 'jscad.example.js')

// Initialize drawer action
drawer.init()
Expand All @@ -79,9 +88,36 @@ export const setSource = (source, path = '/index.js') => {
currentFile = path
}

export function filesChanged(files){
let file
files.forEach(async path=>{
if(path == currentFile){
file = await getFileFn(path)
readSource(file, currentFile)
}else if(path.name === currentFile){
let reader = new FileReader()
reader.onloadend = ()=>{
setSource(reader.result, currentFile)
}
reader.readAsText(path)
}
})
}

async function readSource(file, currentFile){
// Read FileEntry
file.file((file) => {
const reader = new FileReader()
reader.onloadend = () => {
setSource(reader.result, currentFile)
}
reader.readAsText(file)
})
}

export const setFiles = (files) => {
const editorFiles = document.getElementById('editor-files')
if (files.length === 1) {
if (files.length < 2) {
editorNav.classList.remove('visible')
} else {
// Update spinner
Expand All @@ -94,14 +130,7 @@ export const setFiles = (files) => {
button.addEventListener('click', () => {
currentFile = file.fsPath
editorFile.innerHTML = currentFile
// Read FileEntry
file.file((file) => {
const reader = new FileReader()
reader.onloadend = () => {
setSource(reader.result, currentFile)
}
reader.readAsText(file)
})
readSource(file, currentFile)
})
item.appendChild(button)
editorFiles.appendChild(item)
Expand Down
Loading
Loading