diff --git a/extlib/fs/directory-open.mjs b/extlib/fs/directory-open.mjs new file mode 100644 index 0000000..e51d720 --- /dev/null +++ b/extlib/fs/directory-open.mjs @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +import supported from './supported.mjs'; + +const implementation = !supported + ? import('./legacy/directory-open.mjs') + : supported === 'chooseFileSystemEntries' + ? import('./fs-access-legacy/directory-open.mjs') + : import('./fs-access/directory-open.mjs'); + +/** + * For opening directories, dynamically either loads the File System Access API + * module or the legacy method. + */ +export async function directoryOpen(...args) { + return (await implementation).default(...args); +} diff --git a/extlib/fs/file-open.mjs b/extlib/fs/file-open.mjs new file mode 100644 index 0000000..407082c --- /dev/null +++ b/extlib/fs/file-open.mjs @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +import supported from './supported.mjs'; + +const implementation = !supported + ? import('./legacy/file-open.mjs') + : supported === 'chooseFileSystemEntries' + ? import('./fs-access-legacy/file-open.mjs') + : import('./fs-access/file-open.mjs'); + +/** + * For opening files, dynamically either loads the File System Access API module + * or the legacy method. + */ +export async function fileOpen(...args) { + return (await implementation).default(...args); +} diff --git a/extlib/fs/file-save.mjs b/extlib/fs/file-save.mjs new file mode 100644 index 0000000..8364a05 --- /dev/null +++ b/extlib/fs/file-save.mjs @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +import supported from './supported.mjs'; + +const implementation = !supported + ? import('./legacy/file-save.mjs') + : supported === 'chooseFileSystemEntries' + ? import('./fs-access-legacy/file-save.mjs') + : import('./fs-access/file-save.mjs'); + +/** + * For saving files, dynamically either loads the File System Access API module + * or the legacy method. + */ +export async function fileSave(...args) { + return (await implementation).default(...args); +} diff --git a/extlib/fs/fs-access-legacy/directory-open.mjs b/extlib/fs/fs-access-legacy/directory-open.mjs new file mode 100644 index 0000000..e921f95 --- /dev/null +++ b/extlib/fs/fs-access-legacy/directory-open.mjs @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +const getFiles = async (dirHandle, recursive, path = dirHandle.name) => { + const dirs = []; + const files = []; + for await (const entry of dirHandle.getEntries()) { + const nestedPath = `${path}/${entry.name}`; + if (entry.isFile) { + files.push( + entry.getFile().then((file) => + Object.defineProperty(file, 'webkitRelativePath', { + configurable: true, + enumerable: true, + get: () => nestedPath, + }) + ) + ); + } else if (entry.isDirectory && recursive) { + dirs.push(getFiles(entry, recursive, nestedPath)); + } + } + return [...(await Promise.all(dirs)).flat(), ...(await Promise.all(files))]; +}; + +/** + * Opens a directory from disk using the (legacy) File System Access API. + * @type { typeof import("../../index").directoryOpen } + */ +export default async (options = {}) => { + options.recursive = options.recursive || false; + const handle = await window.chooseFileSystemEntries({ + type: 'open-directory', + }); + return getFiles(handle, options.recursive); +}; diff --git a/extlib/fs/fs-access-legacy/file-open.mjs b/extlib/fs/fs-access-legacy/file-open.mjs new file mode 100644 index 0000000..2cdfe6b --- /dev/null +++ b/extlib/fs/fs-access-legacy/file-open.mjs @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +const getFileWithHandle = async (handle) => { + const file = await handle.getFile(); + file.handle = handle; + return file; +}; + +/** + * Opens a file from disk using the (legacy) File System Access API. + * @type { typeof import("../../index").fileOpen } + */ +export default async (options = {}) => { + const handleOrHandles = await window.chooseFileSystemEntries({ + accepts: [ + { + description: options.description || '', + mimeTypes: options.mimeTypes || ['*/*'], + extensions: options.extensions || [''], + }, + ], + multiple: options.multiple || false, + }); + if (options.multiple) { + return Promise.all(handleOrHandles.map(getFileWithHandle)); + } + return getFileWithHandle(handleOrHandles); +}; diff --git a/extlib/fs/fs-access-legacy/file-save.mjs b/extlib/fs/fs-access-legacy/file-save.mjs new file mode 100644 index 0000000..9fe0ca6 --- /dev/null +++ b/extlib/fs/fs-access-legacy/file-save.mjs @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +/** + * Saves a file to disk using the (legacy) File System Access API. + * @type { typeof import("../../index").fileSave } + */ +export default async (blob, options = {}, handle = null) => { + options.fileName = options.fileName || 'Untitled'; + handle = + handle || + (await window.chooseFileSystemEntries({ + type: 'save-file', + accepts: [ + { + description: options.description || '', + mimeTypes: [blob.type], + extensions: options.extensions || [''], + }, + ], + })); + const writable = await handle.createWritable(); + await writable.write(blob); + await writable.close(); + return handle; +}; diff --git a/extlib/fs/fs-access/directory-open.mjs b/extlib/fs/fs-access/directory-open.mjs new file mode 100644 index 0000000..19fade2 --- /dev/null +++ b/extlib/fs/fs-access/directory-open.mjs @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +const getFiles = async (dirHandle, recursive, path = dirHandle.name) => { + const dirs = []; + const files = []; + for await (const entry of dirHandle.values()) { + const nestedPath = `${path}/${entry.name}`; + if (entry.kind === 'file') { + files.push( + entry.getFile().then((file) => + Object.defineProperty(file, 'webkitRelativePath', { + configurable: true, + enumerable: true, + get: () => nestedPath, + }) + ) + ); + } else if (entry.kind === 'directory' && recursive) { + dirs.push(getFiles(entry, recursive, nestedPath)); + } + } + return [...(await Promise.all(dirs)).flat(), ...(await Promise.all(files))]; +}; + +/** + * Opens a directory from disk using the File System Access API. + * @type { typeof import("../../index").directoryOpen } + */ +export default async (options = {}) => { + options.recursive = options.recursive || false; + const handle = await window.showDirectoryPicker(); + return getFiles(handle, options.recursive); +}; diff --git a/extlib/fs/fs-access/file-open.mjs b/extlib/fs/fs-access/file-open.mjs new file mode 100644 index 0000000..dab1119 --- /dev/null +++ b/extlib/fs/fs-access/file-open.mjs @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +const getFileWithHandle = async (handle) => { + const file = await handle.getFile(); + file.handle = handle; + return file; +}; + +/** + * Opens a file from disk using the File System Access API. + * @type { typeof import("../../index").fileOpen } + */ +export default async (options = {}) => { + const accept = {}; + if (options.mimeTypes) { + options.mimeTypes.map((mimeType) => { + accept[mimeType] = options.extensions || []; + }); + } else { + accept['*/*'] = options.extensions || []; + } + const handleOrHandles = await window.showOpenFilePicker({ + types: [ + { + description: options.description || '', + accept: accept, + }, + ], + multiple: options.multiple || false, + }); + const files = await Promise.all(handleOrHandles.map(getFileWithHandle)); + if (options.multiple) { + return files; + } + return files[0]; +}; diff --git a/extlib/fs/fs-access/file-save.mjs b/extlib/fs/fs-access/file-save.mjs new file mode 100644 index 0000000..2ceea07 --- /dev/null +++ b/extlib/fs/fs-access/file-save.mjs @@ -0,0 +1,67 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +/** + * Saves a file to disk using the File System Access API. + * @type { typeof import("../../index").fileSave } + */ +export default async ( + blob, + options = {}, + existingHandle = null, + throwIfExistingHandleNotGood = false +) => { + options.fileName = options.fileName || 'Untitled'; + const accept = {}; + if (options.mimeTypes) { + options.mimeTypes.push(blob.type); + options.mimeTypes.map((mimeType) => { + accept[mimeType] = options.extensions || []; + }); + console.log(accept,'heeeee') + } else { + accept[blob.type] = options.extensions || []; + } + if (existingHandle) { + try { + // Check if the file still exists. + await existingHandle.getFile(); + } catch (err) { + existingHandle = null; + if (throwIfExistingHandleNotGood) { + throw err; + } + } + } + + console.log(accept) + const handle = + existingHandle || + (await window.showSaveFilePicker({ + suggestedName: options.fileName, + types: [ + { + description: options.description || '', + accept: accept, + }, + ], + })); + const writable = await handle.createWritable(); + await writable.write(blob); + await writable.close(); + return handle; +}; diff --git a/extlib/fs/index.js b/extlib/fs/index.js new file mode 100644 index 0000000..f67f6df --- /dev/null +++ b/extlib/fs/index.js @@ -0,0 +1,24 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +/** + * @module browser-fs-access + */ +export { fileOpen } from './file-open.mjs'; +export { directoryOpen } from './directory-open.mjs'; +export { fileSave } from './file-save.mjs'; +export { default as supported } from './supported.mjs'; diff --git a/extlib/fs/legacy/directory-open.mjs b/extlib/fs/legacy/directory-open.mjs new file mode 100644 index 0000000..e58a870 --- /dev/null +++ b/extlib/fs/legacy/directory-open.mjs @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +/** + * Opens a directory from disk using the legacy + * `` method. + * @type { typeof import("../../index").directoryOpen } + */ +export default async (options = {}) => { + options.recursive = options.recursive || false; + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.webkitdirectory = true; + + // ToDo: Remove this workaround once + // https://github.com/whatwg/html/issues/6376 is specified and supported. + const rejectOnPageInteraction = () => { + window.removeEventListener('pointermove', rejectOnPageInteraction); + window.removeEventListener('pointerdown', rejectOnPageInteraction); + window.removeEventListener('keydown', rejectOnPageInteraction); + reject(new DOMException('The user aborted a request.', 'AbortError')); + }; + + window.addEventListener('pointermove', rejectOnPageInteraction); + window.addEventListener('pointerdown', rejectOnPageInteraction); + window.addEventListener('keydown', rejectOnPageInteraction); + + input.addEventListener('change', () => { + window.removeEventListener('pointermove', rejectOnPageInteraction); + window.removeEventListener('pointerdown', rejectOnPageInteraction); + window.removeEventListener('keydown', rejectOnPageInteraction); + let files = Array.from(input.files); + if (!options.recursive) { + files = files.filter((file) => { + return file.webkitRelativePath.split('/').length === 2; + }); + } + resolve(files); + }); + + input.click(); + }); +}; diff --git a/extlib/fs/legacy/file-open.mjs b/extlib/fs/legacy/file-open.mjs new file mode 100644 index 0000000..cfa4f7f --- /dev/null +++ b/extlib/fs/legacy/file-open.mjs @@ -0,0 +1,56 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +/** + * Opens a file from disk using the legacy `` method. + * @type { typeof import("../../index").fileOpen } + */ +export default async (options = {}) => { + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + const accept = [ + ...(options.mimeTypes ? options.mimeTypes : []), + options.extensions ? options.extensions : [], + ].join(); + input.multiple = options.multiple || false; + // Empty string allows everything. + input.accept = accept || ''; + + // ToDo: Remove this workaround once + // https://github.com/whatwg/html/issues/6376 is specified and supported. + const rejectOnPageInteraction = () => { + window.removeEventListener('pointermove', rejectOnPageInteraction); + window.removeEventListener('pointerdown', rejectOnPageInteraction); + window.removeEventListener('keydown', rejectOnPageInteraction); + reject(new DOMException('The user aborted a request.', 'AbortError')); + }; + + window.addEventListener('pointermove', rejectOnPageInteraction); + window.addEventListener('pointerdown', rejectOnPageInteraction); + window.addEventListener('keydown', rejectOnPageInteraction); + + input.addEventListener('change', () => { + window.removeEventListener('pointermove', rejectOnPageInteraction); + window.removeEventListener('pointerdown', rejectOnPageInteraction); + window.removeEventListener('keydown', rejectOnPageInteraction); + resolve(input.multiple ? input.files : input.files[0]); + }); + + input.click(); + }); +}; diff --git a/extlib/fs/legacy/file-save.mjs b/extlib/fs/legacy/file-save.mjs new file mode 100644 index 0000000..da453f7 --- /dev/null +++ b/extlib/fs/legacy/file-save.mjs @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +/** + * Saves a file to disk using the legacy `` method. + * @type { typeof import("../../index").fileSave } + */ +export default async (blob, options = {}) => { + const a = document.createElement('a'); + a.download = options.fileName || 'Untitled'; + a.href = URL.createObjectURL(blob); + a.addEventListener('click', () => { + // `setTimeout()` due to + // https://github.com/LLK/scratch-gui/issues/1783#issuecomment-426286393 + setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000); + }); + a.click(); +}; diff --git a/extlib/fs/supported.mjs b/extlib/fs/supported.mjs new file mode 100644 index 0000000..b1ac42a --- /dev/null +++ b/extlib/fs/supported.mjs @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. + +/** + * Returns whether the File System Access API is supported and usable in the + * current context (for example cross-origin iframes). + * @returns {boolean} Returns `true` if the File System Access API is supported and usable, else returns `false`. + */ +const supported = (() => { + // ToDo: Remove this check once Permissions Policy integration + // has happened, tracked in + // https://github.com/WICG/file-system-access/issues/245. + if ('top' in self && self !== top) { + try { + // This will succeed on same-origin iframes, + // but fail on cross-origin iframes. + top.location + ''; + } catch { + return false; + } + } else if ('chooseFileSystemEntries' in self) { + return 'chooseFileSystemEntries'; + } else if ('showOpenFilePicker' in self) { + return 'showOpenFilePicker'; + } + return false; +})(); + +export default supported; diff --git a/icon/icon-16.png b/icon/icon-16.png index fb1133a..fadd0ed 100644 Binary files a/icon/icon-16.png and b/icon/icon-16.png differ diff --git a/icon/icon-24.png b/icon/icon-24.png index 21f58e2..3d9a7ff 100644 Binary files a/icon/icon-24.png and b/icon/icon-24.png differ diff --git a/icon/icon-32.png b/icon/icon-32.png index 128e66b..b1837e0 100644 Binary files a/icon/icon-32.png and b/icon/icon-32.png differ diff --git a/icon/icon-64.png b/icon/icon-64.png index ba62b6a..10a782f 100644 Binary files a/icon/icon-64.png and b/icon/icon-64.png differ diff --git a/icon/icon2.svg b/icon/icon2.svg index d47a08f..a2e2513 100644 --- a/icon/icon2.svg +++ b/icon/icon2.svg @@ -32,8 +32,8 @@ showgrid="false" inkscape:window-width="1514" inkscape:window-height="1199" - inkscape:window-x="962" - inkscape:window-y="154" + inkscape:window-x="705" + inkscape:window-y="83" inkscape:window-maximized="0" units="px" /> diff --git a/icon/svg-to-favicon b/icon/svg-to-favicon index d57b162..ca66e30 100755 --- a/icon/svg-to-favicon +++ b/icon/svg-to-favicon @@ -1,17 +1,17 @@ #!/usr/bin/env bash -svg=icon2 +svg=./svgr_raw/logo.svg size=(16 24 32 64) out="" for i in ${size[@]}; do - inkscape --export-filename="./icon-$i.png" $svg.svg -w $i -h $i + inkscape --export-filename="./icon-$i.png" $svg -w $i -h $i out+="icon-$i.png " done size=(192 512) for i in ${size[@]}; do - inkscape --export-filename="./icon-$i.png" $svg.svg -w $i -h $i + inkscape --export-filename="./icon-$i.png" $svg -w $i -h $i done convert $out favicon.ico diff --git a/icon/svgr_raw/coincident_alt.svg b/icon/svgr_raw/coincident_alt.svg new file mode 100644 index 0000000..d89c510 --- /dev/null +++ b/icon/svgr_raw/coincident_alt.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/icon/svgr_raw/logo.svg b/icon/svgr_raw/logo.svg index d47a08f..a2e2513 100644 --- a/icon/svgr_raw/logo.svg +++ b/icon/svgr_raw/logo.svg @@ -32,8 +32,8 @@ showgrid="false" inkscape:window-width="1514" inkscape:window-height="1199" - inkscape:window-x="962" - inkscape:window-y="154" + inkscape:window-x="705" + inkscape:window-y="83" inkscape:window-maximized="0" units="px" /> diff --git a/src/Scene.js b/src/Scene.js index b75cb3a..a53cd5f 100644 --- a/src/Scene.js +++ b/src/Scene.js @@ -16,7 +16,7 @@ let stats if (process.env.NODE_ENV !== 'production') { const { default: d } = require('../extlib/stats.module.js') stats = new d(); - document.getElementById('stats').appendChild(stats.dom); + // document.getElementById('stats').appendChild(stats.dom); } @@ -137,14 +137,15 @@ export class Scene { helpersGroup.add(light2); - this.render = render.bind(this); - this.addSketch = addSketch.bind(this); - this.extrude = this.extrude.bind(this); - this.onHover = onHover.bind(this); - this.onPick = onPick.bind(this); - this.clearSelection = clearSelection.bind(this); - this.setHover = setHover.bind(this); - this.awaitSelection = awaitSelection.bind(this); + this.render = render.bind(this) + this.addSketch = addSketch.bind(this) + this.onHover = onHover.bind(this) + this.onPick = onPick.bind(this) + this.clearSelection = clearSelection.bind(this) + this.setHover = setHover.bind(this) + this.awaitSelection = awaitSelection.bind(this) + this.extrude = this.extrude.bind(this) + this.obj3d.addEventListener('change', this.render); this.controls.addEventListener('change', this.render); @@ -153,20 +154,32 @@ export class Scene { if (process.env.NODE_ENV !== 'production') { this.stats = stats - this.stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom - document.getElementById('stats').appendChild(this.stats.dom); } - this.hovered = []; - this.selected = []; this.activeSketch = null; + this.selected = []; + this.mode = ''; + this.store.subscribe(this.reduxCallback.bind(this)) + this.render(); } + reduxCallback() { + const currSelected = this.store.getState().ui.selectedList + const currMode = this.store.getState().ui.mode + if (currSelected !== this.selected) { + this.selected = currSelected + } + if (currMode !== this.mode) { + this.mode = currMode + } + + } + resizeCanvas(renderer) { diff --git a/src/Sketch.js b/src/Sketch.js index d323891..bcb1c12 100644 --- a/src/Sketch.js +++ b/src/Sketch.js @@ -2,7 +2,7 @@ import * as THREE from '../node_modules/three/src/Three'; -import { _vec2, _vec3, raycaster, awaitSelection, ptObj, setHover,custPtMat } from './shared' +import { _vec2, _vec3, raycaster, awaitSelection, ptObj, setHover, custPtMat } from './shared' import { drawOnClick1, drawOnClick2, drawPreClick2, drawOnClick3, drawPreClick3, drawClear, drawPoint } from './drawEvents' import { onHover, onDrag, onPick, onRelease, clearSelection } from './mouseEvents' @@ -47,10 +47,11 @@ class Sketch { this.constraints = new Map() this.c_id = 1; - this.obj3d.add(new THREE.Group()); + this.dimGroup = new THREE.Group() + this.dimGroup.name = 'dimensions' + this.obj3d.add(this.dimGroup); this.geomStartIdx = this.obj3d.children.length this.obj3d.userData.geomStartIdx = this.geomStartIdx - this.dimGroup = this.obj3d.children[this.geomStartIdx - 1] this.labels = [] @@ -101,9 +102,11 @@ class Sketch { this.bindHandlers() - this.selected = [] + // this.selected = this.scene.selected + // this.selected = [] + this.hovered = [] - this.mode = "" + this.scene.mode = "" this.subsequent = false; } @@ -155,7 +158,7 @@ class Sketch { this.c_idOnActivate = this.c_id const changeDetector = (e) => { - if (this.selected.length && e.buttons) { + if (this.scene.selected.length && e.buttons) { this.canvas.removeEventListener('pointermove', changeDetector) this.hasChanged = true } @@ -230,93 +233,110 @@ class Sketch { onKeyPress(e) { - this.command(e.key) + + if (e.isTrusted && e.key == 'Escape') { + drawClear.call(this) + document.activeElement.blur() + + this.scene.store.dispatch({ type: 'set-mode', mode: '' }) + } else { + const keyToMode = { + l: 'line', + a: 'arc', + p: 'point', + d: 'dimension', + c: 'coincident', + v: 'vertical', + h: 'horizontal', + t: 'tangent', + 'Delete': 'delete', + 'Backspace': 'delete' + } + this.command(keyToMode[e.key]) + console.log(e.key) + } } - command(key) { - switch (key) { - case 'Escape': - drawClear.call(this) - document.activeElement.blur() - break; - case 'l': - if (this.mode == 'line') { - drawClear.call(this) - } - this.mode = "line" - this.snap = true - this.canvas.addEventListener('pointerdown', this.drawOnClick1, { once: true }) - break; - case 'a': - this.mode = "arc" - this.snap = true - this.canvas.addEventListener('pointerdown', this.drawOnClick1, { once: true }) - // this.canvas.addEventListener('pointerdown', this.drawOnClick1, { once: true }) - break; - case 'p': - this.mode = "point" - this.snap = true - this.canvas.addEventListener('pointerdown', (e) => { - if (this.mode !== 'point') return - const pt = ptObj() - pt.matrixAutoUpdate = false; - pt.userData.constraints = [] + command(com) { + drawClear.call(this) + document.activeElement.blur() - pt.geometry.attributes.position.set( - this.getLocation(e).toArray() - ) - pt.layers.enable(2) + let mode; - this.obj3d.add(pt) - this.updatePointsBuffer(this.obj3d.children.length - 1) - this.scene.render() - }) - break; - case 'd': - if (this.mode != 'dimension') { - drawClear.call(this) - this.mode = "dimension" - this.drawDimension() - } - break; - case 'c': - drawClear.call(this) - setCoincident.call(this) - break; - case 'v': - drawClear.call(this) - setOrdinate.call(this, 0) - break; - case 'h': - drawClear.call(this) - setOrdinate.call(this, 1) - break; - case 't': - drawClear.call(this) - setTangent.call(this) - break; - case 'Delete': + switch (com) { + case 'delete': this.deleteSelected() break; - case 'Backspace': - this.deleteSelected() - break; - case 'z': - console.log('undo would be nice') + case 'coincident': + case 'vertical': + case 'horizontal': + case 'tangent': + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); + case 'line': + case 'arc': + case 'point': + case 'dimension': + if (this.scene.mode == com) { + mode = '' + } else { + mode = com + + switch (com) { + case 'line': + case 'arc': + this.canvas.addEventListener('pointerdown', this.drawOnClick1, { once: true }); + break; + case 'point': + this.canvas.addEventListener('pointerdown', (e) => { + if (this.scene.mode !== 'point') return + const pt = ptObj() + + pt.matrixAutoUpdate = false; + pt.userData.constraints = [] + + pt.geometry.attributes.position.set( + this.getLocation(e).toArray() + ) + pt.layers.enable(2) + + this.obj3d.add(pt) + this.updatePointsBuffer(this.obj3d.children.length - 1) + this.scene.render() + }); + break; + case 'dimension': + this.drawDimension(); + break; + case 'coincident': + case 'vertical': + case 'horizontal': + case 'tangent': + + setCoincident.call(this).then( + () => this.scene.store.dispatch({ type: 'set-mode', mode: "" }) + ); + break; + } + } break; } - // console.log('this mode:', this.mode) + + if (mode !== undefined) { + this.scene.store.dispatch({ type: 'set-mode', mode }) + } + + // console.log('this mode:', this.scene.mode) } deleteSelected() { - this.selected + this.scene.selected .filter(e => e.userData.type == 'dimension') .forEach(e => this.constraints.has(e.name) && this.deleteConstraints(e.name)) - const toDelete = this.selected + const toDelete = this.scene.selected .filter(e => e.userData.type == 'line') .sort((a, b) => b.id - a.id) .map(obj => { @@ -329,7 +349,10 @@ class Sketch { this.updateOtherBuffers() - this.selected = [] + // this.selected = [] + + this.scene.store.dispatch({ type: 'clear-selection' }) + this.scene.render() } @@ -608,9 +631,12 @@ Object.assign(Sketch.prototype, h_dist: 34, v_dist: 35, }, + + max_pts: 1000, max_links: 1000, max_constraints: 1000, + } ) diff --git a/src/constraintEvents.js b/src/constraintEvents.js index 0f2061e..a90d96a 100644 --- a/src/constraintEvents.js +++ b/src/constraintEvents.js @@ -1,4 +1,3 @@ -import { color, setHover } from './shared' export async function setCoincident(sel) { let selection @@ -37,12 +36,15 @@ export async function setCoincident(sel) { this.solve() this.updateBoundingSpheres() - for (let x = 0; x < this.selected.length; x++) { - const obj = this.selected[x] - setHover(obj, 0) - } - this.selected = [] + this.clearSelection() + // for (let x = 0; x < this.scene.selected.length; x++) { + // const obj = this.selected[x] + // setHover(obj, 0) + // } + + // this.selected = [] + this.scene.render() } @@ -52,7 +54,7 @@ export async function setOrdinate(dir = 0) { if (selection == null) return; let arr - if (this.selected.length == 1) { + if (selection.length == 1) { arr = [-1, -1, selection[0].name, -1] } else { arr = [selection[0].name, selection[1].name, -1, -1] @@ -72,12 +74,8 @@ export async function setOrdinate(dir = 0) { this.updateOtherBuffers() this.solve() this.updateBoundingSpheres() - for (let x = 0; x < this.selected.length; x++) { - const obj = this.selected[x] - setHover(obj, 0) - } - this.selected = [] + this.clearSelection() this.scene.render() } @@ -156,12 +154,8 @@ export async function setTangent() { this.solve() this.updateBoundingSpheres() - for (let x = 0; x < this.selected.length; x++) { - const obj = this.selected[x] - setHover(obj, 0) - } - - this.selected = [] + this.clearSelection() + this.scene.render() } diff --git a/src/drawDimension.js b/src/drawDimension.js index 29f87d4..6c197ee 100644 --- a/src/drawDimension.js +++ b/src/drawDimension.js @@ -157,7 +157,7 @@ export async function drawDimension() { this.labelContainer.removeChild(this.labelContainer.lastChild); this.scene.render() } - if (this.mode == "dimension") { + if (this.scene.mode == "dimension") { this.drawDimension() } diff --git a/src/drawEvents.js b/src/drawEvents.js index 2e84a5d..dbff600 100644 --- a/src/drawEvents.js +++ b/src/drawEvents.js @@ -1,10 +1,9 @@ import { drawArc, drawArc2, drawArc3, drawArc4 } from './drawArc' import { drawLine, drawLine2 } from './drawLine' -import { ptObj } from './shared' -export function drawOnClick1(e, loc) { - if (!loc && e.buttons !== 1) return +export function drawOnClick1(e) { + if (e.buttons !== 1) return // this.canvas.removeEventListener('pointerdown', this.drawOnClick1) @@ -14,9 +13,7 @@ export function drawOnClick1(e, loc) { let mouseLoc - if (loc) { - mouseLoc = loc - } else if (this.hovered.length && !this.subsequent) { + if (this.hovered.length && !this.subsequent) { mouseLoc = this.hovered[this.hovered.length - 1].geometry.attributes.position.array } else { mouseLoc = this.getLocation(e).toArray(); @@ -24,8 +21,8 @@ export function drawOnClick1(e, loc) { - // this.mode allow alow following modes to create new obj3ds - if (this.mode == "line") { + // this.scene.mode allow alow following modes to create new obj3ds + if (this.scene.mode == "line") { this.toPush = drawLine(mouseLoc) if (this.subsequent) { // we pre-increment because we need to push the same c_id to the constraints @@ -41,7 +38,7 @@ export function drawOnClick1(e, loc) { this.toPush[0].userData.constraints.push(this.c_id) } - } else if (this.mode == "arc") { + } else if (this.scene.mode == "arc") { this.toPush = drawArc(mouseLoc) } @@ -59,7 +56,7 @@ export function drawOnClick1(e, loc) { this.updatePoint = this.obj3d.children.length this.obj3d.add(...this.toPush) - this.linkedObjs.set(this.l_id, [this.mode, this.toPush.map(e => e.name)]) + this.linkedObjs.set(this.l_id, [this.scene.mode, this.toPush.map(e => e.name)]) for (let obj of this.toPush) { obj.userData.l_id = this.l_id } @@ -71,9 +68,9 @@ export function drawOnClick1(e, loc) { export function drawPreClick2(e) { const mouseLoc = this.getLocation(e).toArray(); - if (this.mode == "line") { + if (this.scene.mode == "line") { drawLine2(mouseLoc, this.toPush) - } else if (this.mode == 'arc') { + } else if (this.scene.mode == 'arc') { drawArc2(mouseLoc, this.toPush) } @@ -88,7 +85,7 @@ export function drawOnClick2(e) { this.updatePointsBuffer(this.updatePoint) this.updateOtherBuffers() - // a this.mode == "" (set with esc) here will prevent event chain from persisisting + // a this.scene.mode == "" (set with esc) here will prevent event chain from persisisting this.toPush.forEach(element => { // make sure elements are selectable by sketch raycaster element.layers.enable(2) @@ -109,16 +106,21 @@ export function drawOnClick2(e) { modLoc = this.hovered[this.hovered.length - 1].geometry.attributes.position.array } - if (this.mode == "line") { - this.subsequent = true + if (this.scene.mode == "line") { if (modLoc) { drawLine2(modLoc, this.toPush) - this.drawOnClick1(null, modLoc) + // this.drawOnClick1(null, modLoc) + + this.subsequent = false + this.updatePoint = this.obj3d.children.length + this.canvas.addEventListener('pointerdown', this.drawOnClick1, { once: true }) + } else { + this.subsequent = true this.drawOnClick1(e) } - } else if (this.mode == "arc") { + } else if (this.scene.mode == "arc") { if (modLoc) { drawArc2(modLoc, this.toPush) } @@ -134,6 +136,7 @@ export function drawOnClick2(e) { let ccw; export function drawPreClick3(e) { + this.noHover = true const mouseLoc = this.getLocation(e); ccw = drawArc4(mouseLoc, this.toPush) this.scene.render() @@ -141,6 +144,7 @@ export function drawPreClick3(e) { export function drawOnClick3(e) { if (e.buttons !== 1) return; + this.noHover = false this.canvas.removeEventListener('pointermove', this.drawPreClick3); if (!ccw) { @@ -164,9 +168,8 @@ export function drawOnClick3(e) { export function drawClear() { - if (this.mode == "") return - - if (['line', 'arc'].includes(this.mode)) { + if (this.scene.mode == "") return + if (['line', 'arc'].includes(this.scene.mode)) { this.delete(this.obj3d.children[this.updatePoint]) } @@ -179,9 +182,7 @@ export function drawClear() { this.scene.render() this.subsequent = false this.toPush = [] - this.snap = false - this.mode = "" } diff --git a/src/mouseEvents.js b/src/mouseEvents.js index 5c6be46..e3e9eb9 100644 --- a/src/mouseEvents.js +++ b/src/mouseEvents.js @@ -5,8 +5,7 @@ import { onDimMoveEnd } from './drawDimension' let ptLoc export function onHover(e) { - // if ((this.mode && this.mode != 'dimension' && !this.snap) || e.buttons) return - if (e.buttons) return + if (e.buttons || this.noHover) return raycaster.setFromCamera( new THREE.Vector2( @@ -46,13 +45,14 @@ export function onHover(e) { } } - if (!idx.length && !this.snap) { + if (!idx.length) { idx.push(0) } } + const selected = this.selected || this.scene.selected if (idx.length) { // after filtering, if hovered objs still exists if ( !this.hovered.length @@ -63,7 +63,7 @@ export function onHover(e) { for (let x = 0; x < this.hovered.length; x++) { // first clear old hovers that are not selected const obj = this.hovered[x] - if (typeof obj == 'object' && !this.selected.includes(obj)) { + if (typeof obj == 'object' && !selected.includes(obj)) { setHover(obj, 0) } } @@ -107,7 +107,7 @@ export function onHover(e) { if (typeof obj == 'number') { this.selpoints[0].visible = false - } else if (!this.selected.includes(obj)) { + } else if (!selected.includes(obj)) { setHover(obj, 0) } @@ -124,13 +124,19 @@ export function onHover(e) { let draggedLabel; export function onPick(e) { - if ((this.mode && this.mode != 'dimension') || e.buttons != 1) return + // console.log('aa',this.scene.mode) + // if ((this.scene && this.scene.mode.length && this.scene.mode != 'dimension') || e.buttons != 1) return + // console.log('bb') + + if ((this.scene && ['line', 'arc'].includes(this.scene.mode)) || e.buttons != 1) return + + const store = this.store || this.scene.store + const selected = this.selected || this.scene.selected if (this.hovered.length) { let obj = this.hovered[this.hovered.length - 1] - // if (sc.selected.includes(obj3d)) continue - if (typeof obj != 'object') { // special sketchplace define pts in feature mode + if (typeof obj != 'object') { // special case define pts in feature mode const pp = this.selpoints[this.fptIdx % 3 + 1] const p0 = this.selpoints[0] @@ -142,73 +148,68 @@ export function onPick(e) { obj = pp this.fptIdx++ + } - const idx = this.selected.indexOf(obj) + + store.dispatch({ type: 'on-pick', obj }) + + + // if (idx == -1) { + // this.selected.push( + // obj + // ) + // } else if (obj.userData.type != 'selpoint') { + // this.selected.splice(idx, 1) + // } + + const idx = selected.indexOf(obj) + + if (obj.userData.type != 'selpoint') { if (idx == -1) { - this.selected.push( - obj - ) - } else { - this.selected.splice(idx, 1, obj) - } - - } else { - - const idx = this.selected.indexOf(obj) - if (idx == -1) { - this.selected.push( - obj - ) this.setHover(obj, 1) - } else { - - this.setHover(this.selected[idx], 0) - - this.selected.splice(idx, 1) - + this.setHover(selected[idx], 0) } } this.obj3d.dispatchEvent({ type: 'change' }) - if (this.obj3d.userData.type != 'sketch') { - return; - } + if (this.obj3d.userData.type == 'sketch') { + switch (obj.userData.type) { + case 'dimension': + const idx = this.dimGroup.children.indexOf(this.hovered[0]) + if (idx % 2) { // we only allow tag point (odd idx) to be dragged + this.onDragDim = this._onMoveDimension( + this.dimGroup.children[idx], + this.dimGroup.children[idx - 1], + ) + this.canvas.addEventListener('pointermove', this.onDragDim); + this.canvas.addEventListener('pointerup', () => { + onDimMoveEnd(this.dimGroup.children[idx]) + this.onRelease() + }) + } - switch (obj.userData.type) { - case 'dimension': - const idx = this.dimGroup.children.indexOf(this.hovered[0]) - if (idx % 2) { // we only allow tag point (odd idx) to be dragged - this.onDragDim = this._onMoveDimension( - this.dimGroup.children[idx], - this.dimGroup.children[idx - 1], - ) - this.canvas.addEventListener('pointermove', this.onDragDim); - this.canvas.addEventListener('pointerup', () => { - onDimMoveEnd(this.dimGroup.children[idx]) - this.onRelease() - }) - } + draggedLabel = this.dimGroup.children[idx].label + draggedLabel.style.zIndex = -1; + break; + case 'point': - draggedLabel = this.dimGroup.children[idx].label - draggedLabel.style.zIndex = -1; - break; - case 'point': + this.canvas.addEventListener('pointermove', this.onDrag); + this.canvas.addEventListener('pointerup', this.onRelease) + break; - this.canvas.addEventListener('pointermove', this.onDrag); - this.canvas.addEventListener('pointerup', this.onRelease) - break; - - default: - break; + default: + break; + } } } else { - for (let x = 0; x < this.selected.length; x++) { - const obj = this.selected[x] + const vis = store.getState().treeEntries.visible + for (let x = 0, obj; x < selected.length; x++) { + obj = selected[x] if (obj.userData.type == 'selpoint') { @@ -219,15 +220,14 @@ export function onPick(e) { } // dont think this would have been possible without redux - // if (obj.userData.type == 'sketch' && !sc.store.getState().treeEntries.visible[obj.name]) { - if (obj.userData.type == 'sketch' && !this.scene.store.getState().treeEntries.visible[obj.name]) { + if (obj.userData.type == 'sketch' && !vis[obj.name]) { obj.visible = false } - } + store.dispatch({ type: 'clear-selection' }) + this.obj3d.dispatchEvent({ type: 'change' }) - this.selected = [] } } @@ -267,15 +267,17 @@ export function onRelease(e) { } export function clearSelection() { - for (let x = 0, obj; x < this.selected.length; x++) { - obj = this.selected[x] + const selected = this.selected || this.scene.selected + for (let x = 0, obj; x < selected.length; x++) { + obj = selected[x] if (obj.userData.type == 'selpoint') { obj.visible = false } else { setHover(obj, 0) } } - this.selected = [] + + store.dispatch({ type: 'clear-selection' }) for (let x = 0; x < this.hovered.length; x++) { diff --git a/src/react/app.css b/src/react/app.css index 53c7509..db568ee 100644 --- a/src/react/app.css +++ b/src/react/app.css @@ -3,6 +3,8 @@ * { box-sizing: border-box; + scrollbar-color: lightgray #262626; + scrollbar-width: thin; } body { @@ -61,7 +63,7 @@ body { cursor: pointer; @apply fill-current bg-transparent text-gray-200 - hover:text-green-400; + hover:text-green-500; } @@ -95,3 +97,21 @@ input[type=number] { .drop-down-top { top: calc(var(--topNavH) + 6px); } + + +.hide-scroll{ + scrollbar-width: none; /* Firefox */ +} + +.hide-scroll::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +::-webkit-scrollbar { + width: 0.375rem; + background: #262626; +} + +::-webkit-scrollbar-thumb { + background: lightgray; +} diff --git a/src/react/app.jsx b/src/react/app.jsx index f0b8161..8197d1b 100644 --- a/src/react/app.jsx +++ b/src/react/app.jsx @@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', async () => { const { Scene } = await import('../Scene') sce = new Scene(store) - // window.sc = sce + window.sc = sce ReactDOM.render(, document.getElementById('react')); diff --git a/src/react/dialog.jsx b/src/react/dialog.jsx index 02bca3c..55ddf2c 100644 --- a/src/react/dialog.jsx +++ b/src/react/dialog.jsx @@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'react-redux' import { MdDone, MdClose } from 'react-icons/md' import * as Icon from "./icons"; -import {sce} from './app' +import { sce } from './app' export const Dialog = () => { @@ -17,19 +17,26 @@ export const Dialog = () => { const ref = useRef() + const target = treeEntries.byId[dialog.target] + + // console.log(dialog, treeEntries) + useEffect(() => { if (!ref.current) return + + + ref.current.focus() }, [dialog]) const extrude = () => { - const mesh = sce.extrude(dialog.target, ref.current.value) + const mesh = sce.extrude(target, ref.current.value) - dispatch({ type: 'rx-extrusion', mesh, sketchId: dialog.target.obj3d.name }) + dispatch({ type: 'rx-extrusion', mesh, sketchId: target.obj3d.name }) - if (sce.activeSketch == dialog.target) { + if (sce.activeSketch == target) { dispatch({ type: 'finish-sketch' }) - dialog.target.deactivate() + target.deactivate() } dispatch({ type: "clear-dialog" }) @@ -38,7 +45,7 @@ export const Dialog = () => { } const extrudeCancel = () => { - if (sce.activeSketch == dialog.target) { // if extrude dialog launched from sketch mode we set dialog back to the sketch dialog + if (sce.activeSketch == target) { // if extrude dialog launched from sketch mode we set dialog back to the sketch dialog dispatch({ type: 'set-dialog', action: 'sketch' }) } else { dispatch({ type: "clear-dialog" }) @@ -46,9 +53,9 @@ export const Dialog = () => { } const extrudeEdit = () => { - dialog.target.userData.featureInfo[1] = ref.current.value + target.userData.featureInfo[1] = ref.current.value - sce.refreshNode(dialog.target.name, treeEntries) + sce.refreshNode(target.name, treeEntries) dispatch({ type: 'set-modified', status: true }) dispatch({ type: "clear-dialog" }) @@ -76,17 +83,14 @@ export const Dialog = () => { } const sketchCancel = () => { - console.log(sce.activeSketch.hasChanged) - if (sce.activeSketch.hasChanged + if (sce.newSketch) { + dispatch({ type: 'delete-node', id: sce.activeSketch.obj3d.name }) + sce.sid -= 1 + } else if (sce.activeSketch.hasChanged || sce.activeSketch.idOnActivate != id || sce.activeSketch.c_idOnActivate != sce.activeSketch.c_id ) { - if (sce.newSketch) { - dispatch({ type: 'delete-node', id: sce.activeSketch.obj3d.name }) - sce.sid -= 1 - } else { - dispatch({ type: "restore-sketch" }) - } + dispatch({ type: "restore-sketch" }) } dispatch({ type: 'finish-sketch' }) @@ -113,7 +117,7 @@ export const Dialog = () => { case 'extrude-edit': return <> - + ref.current.value *= -1} /> @@ -121,7 +125,7 @@ export const Dialog = () => { className="btn w-auto h-full p-3.5 text-green-500" onClick={extrudeEdit} /> - diff --git a/src/react/dropDown.jsx b/src/react/dropDown.jsx index 4b02750..93c0750 100644 --- a/src/react/dropDown.jsx +++ b/src/react/dropDown.jsx @@ -10,7 +10,7 @@ const utf8decoder = new TextDecoder(); export const DropDown = () => { const arr = [ - ['https://raw.githubusercontent.com/twpride/threeCAD/master/demo_parts/headphones-stand.json.gz', 'headphones stand'], + ['https://raw.githubusercontent.com/twpride/threeCAD/master/demo_parts/headphones-stand.json.gz', 'headphones-stand'], ] const dispatch = useDispatch() @@ -53,7 +53,7 @@ export const DropDown = () => { ) ) - dispatch({ type: 'restore-state', state }) + dispatch({ type: 'restore-state', state, fileName:arr[idx][1]}) sce.render() } } diff --git a/src/react/fileHelpers.js b/src/react/fileHelpers.js index bcd71c3..4511190 100644 --- a/src/react/fileHelpers.js +++ b/src/react/fileHelpers.js @@ -1,16 +1,16 @@ -// import { -// fileOpen, -// fileSave, -// } from '../../extlib/fs/index'; - import { fileOpen, fileSave, -} from 'browser-fs-access'; +} from '../../extlib/fs/index'; -import {sce} from './app' +// import { +// fileOpen, +// fileSave, +// } from 'browser-fs-access'; + +import { sce } from './app' // https://web.dev/file-system-access/ @@ -32,13 +32,14 @@ export function STLExport(filename) { } -export async function saveFile(fileHandle, file, dispatch) { +export async function saveFile(fileHandle, file, dispatch, suggestedName) { try { if (!fileHandle) { - return await saveFileAs(file, dispatch); + return await saveFileAs(file, dispatch, suggestedName); } - await fileSave(new Blob([file], { type: 'application/json' }), undefined, fileHandle, true) + const blob = new Blob([file], { type: 'application/json' }) + await fileSave(blob, undefined, fileHandle, true) dispatch({ type: 'set-modified', status: false }) } catch (ex) { @@ -48,12 +49,22 @@ export async function saveFile(fileHandle, file, dispatch) { } }; -export async function saveFileAs(file, dispatch) { +const options = { + mimeTypes: ['application/json'], + extensions: ['.json'], + multiple: false, + description: 'Part files', +}; + +export async function saveFileAs(file, dispatch, suggestedName) { try { - const fileHandle = await fileSave(new Blob([file], { type: 'application/json' }), { - extensions: ['.json'], - }) + + const blob = new Blob([file], { type: 'application/json' }) + + options.fileName = suggestedName + options.extensions[0] + + const fileHandle = await fileSave(blob, options) dispatch({ type: 'set-file-handle', fileHandle, modified: false }) @@ -73,13 +84,7 @@ export async function openFile(dispatch) { try { - const options = { - mimeTypes: ['application/json'], - extensions: ['.json'], - multiple: false, - description: 'Part files', - }; - + file = await fileOpen(options); } catch (ex) { @@ -92,10 +97,13 @@ export async function openFile(dispatch) { } try { - const text = await file.text();; + const text = await file.text(); + console.log(file, file.handle) - dispatch({ type: 'restore-state', state: sce.loadState(text) }) - dispatch({ type: 'set-file-handle', fileHandle:file.handle }) + dispatch({ type: 'restore-state', state: sce.loadState(text), fileName: file.name }) + if (file.handle) { + dispatch({ type: 'set-file-handle', fileHandle: file.handle }) + } } catch (ex) { const msg = `An error occured reading ${fileHandle}`; diff --git a/src/react/icons.jsx b/src/react/icons.jsx index 1e40e1f..cf20ab3 100644 --- a/src/react/icons.jsx +++ b/src/react/icons.jsx @@ -53,6 +53,137 @@ function Coincident(props) { ); } +function Coincident_alt(props) { + return ( + + + + + + + + + + + + ); +} + function Dimension(props) { return ( - + @@ -475,4 +606,4 @@ function Vertical(props) { ); } -export { Arc, Coincident, Dimension, Extrude, Flip, Horizontal, Intersect, Line, Logo, Stl, Subtract, Tangent, Union, Vertical }; \ No newline at end of file +export { Arc, Coincident, Coincident_alt, Dimension, Extrude, Flip, Horizontal, Intersect, Line, Logo, Stl, Subtract, Tangent, Union, Vertical }; \ No newline at end of file diff --git a/src/react/navBar.jsx b/src/react/navBar.jsx index 6e16f6e..9ab3897 100644 --- a/src/react/navBar.jsx +++ b/src/react/navBar.jsx @@ -10,16 +10,32 @@ import { MdSave, MdFolder, MdInsertDriveFile } from 'react-icons/md' import * as Icon from "./icons"; import { Dialog } from './dialog' import { DropDown } from './dropDown' -import { STLExport, saveFile, openFile, verifyPermission } from './fileHelpers' +import { STLExport, saveFile, openFile } from './fileHelpers' + +import { drawClear } from '../drawEvents' import { sce } from './app' + + +const buttonIdx = { + 'line': 1, + 'arc': 2, + 'dimension': 3, + 'coincident': 4, + 'vertical': 5, + 'horizontal': 6, + 'tangent': 7, +} + export const NavBar = () => { const dispatch = useDispatch() - const sketchActive = useSelector(state => state.ui.sketchActive) const treeEntries = useSelector(state => state.treeEntries) + const sketchActive = useSelector(state => state.ui.sketchActive) const fileHandle = useSelector(state => state.ui.fileHandle) const modified = useSelector(state => state.ui.modified) + const fileName = useSelector(state => state.ui.fileName) + const mode = useSelector(state => state.ui.mode) const boolOp = (code) => { if (sce.selected.length != 2 || !sce.selected.every(e => e.userData.type == 'mesh')) { @@ -62,8 +78,9 @@ export const NavBar = () => { sketch.activate() sce.render() + console.log(sketch) - dispatch({ type: 'set-dialog', action: 'sketch' }) + dispatch({ type: 'set-dialog', action: 'sketch', target: sketch.obj3d.name }) forceUpdate() } @@ -105,20 +122,19 @@ export const NavBar = () => { const sketchModeButtons = [ [Icon.Extrude, () => { - dispatch({ type: 'set-dialog', action: 'extrude', target: sce.activeSketch }) - + drawClear.call(sce.activeSketch) + dispatch({ type: 'set-dialog', action: 'extrude', target: sce.activeSketch.obj3d.name }) }, 'Extrude'], - [Icon.Line, () => sce.activeSketch.command('l'), 'Line (L)'], - [Icon.Arc, () => sce.activeSketch.command('a'), 'Arc (A)'], - [Icon.Dimension, () => sce.activeSketch.command('d'), 'Dimension (D)'], - [Icon.Coincident, () => sce.activeSketch.command('c'), 'Coincident (C)'], - [Icon.Vertical, () => sce.activeSketch.command('v'), 'Vertical (V)'], - [Icon.Horizontal, () => sce.activeSketch.command('h'), 'Horizontal (H)'], - [Icon.Tangent, () => sce.activeSketch.command('t'), 'Tangent (T)'], + [Icon.Line, () => sce.activeSketch.command('line'), 'Line (L)'], //1 + [Icon.Arc, () => sce.activeSketch.command('arc'), 'Arc (A)'], + [Icon.Dimension, () => sce.activeSketch.command('dimension'), 'Dimension (D)'], + [Icon.Coincident_alt, () => sce.activeSketch.command('coincident'), 'Coincident (C)'], + [Icon.Vertical, () => sce.activeSketch.command('vertical'), 'Vertical (V)'], + [Icon.Horizontal, () => sce.activeSketch.command('horizontal'), 'Horizontal (H)'], + [Icon.Tangent, () => sce.activeSketch.command('tangent'), 'Tangent (T)'], //7 [MdSave, async () => { - saveFile(fileHandle, JSON.stringify([id, sce.sid, sce.mid, treeEntries]), dispatch) - // saveFile(fileHandle, bson.serialize([id, sce.sid, sce.mid, treeEntries]), dispatch) + saveFile(fileHandle, JSON.stringify([id, sce.sid, sce.mid, treeEntries]), dispatch, fileName) } , 'Save'], ] @@ -128,7 +144,7 @@ export const NavBar = () => { [FaEdit, addSketch, 'Sketch'], [Icon.Extrude, () => { try { - dispatch({ type: 'set-dialog', action: 'extrude', target: treeEntries.byId[sce.selected[0].name] }) + dispatch({ type: 'set-dialog', action: 'extrude', target: sce.selected[0].name }) } catch (err) { console.error(err) alert('please select a sketch from the left pane extrude') @@ -147,8 +163,7 @@ export const NavBar = () => { }, 'New'], [MdSave, () => { - saveFile(fileHandle, JSON.stringify([id, sce.sid, sce.mid, treeEntries]), dispatch) - // saveFile(fileHandle, bson.serialize([id, sce.sid, sce.mid, treeEntries.toJson()]), dispatch) + saveFile(fileHandle, JSON.stringify([id, sce.sid, sce.mid, treeEntries]), dispatch, fileName) } , 'Save'], [MdFolder, () => { @@ -170,7 +185,6 @@ export const NavBar = () => { return
- {/*
*/}
@@ -184,7 +198,8 @@ export const NavBar = () => { {(sketchActive ? sketchModeButtons : partModeButtons).map( ([Icon, fcn, txt], idx) => ( Icon !== undefined ? - :
diff --git a/src/react/reducer.js b/src/react/reducer.js index 68c868a..40b20c4 100644 --- a/src/react/reducer.js +++ b/src/react/reducer.js @@ -5,7 +5,7 @@ import update from 'immutability-helper' import { combineReducers } from 'redux'; import { sce } from './app' -const defaultState = { +const defaultTreeState = { byId: {}, allIds: [], tree: {}, @@ -15,7 +15,7 @@ const defaultState = { let cache -export function treeEntries(state = defaultState, action) { +export function treeEntries(state = defaultTreeState, action) { switch (action.type) { case 'rx-sketch': return update(state, { @@ -96,13 +96,22 @@ export function treeEntries(state = defaultState, action) { case 'restore-state': return action.state case 'new-part': - return defaultState + return defaultTreeState default: return state } } -export function ui(state = { dialog: {}, filePane: false }, action) { +const defaultUIState = { + dialog: {}, + fileHandle: null, + fileName: 'Untitled', + selectedList: [], + selectedSet: {}, + +} + +export function ui(state = defaultUIState, action) { switch (action.type) { case 'set-active-sketch': return update(state, { @@ -119,10 +128,12 @@ export function ui(state = { dialog: {}, filePane: false }, action) { case 'set-dialog': return update(state, { dialog: { $set: { target: action.target, action: action.action } }, + mode: { $set: "" } // we clear the existing mode when entering dialog }) case 'clear-dialog': return update(state, { dialog: { $set: {} }, + mode: { $set: "" } }) case 'set-file-handle': return update(state, { @@ -130,10 +141,7 @@ export function ui(state = { dialog: {}, filePane: false }, action) { modified: { $set: false }, }) case 'new-part': - return update(state, { - fileHandle: { $set: null }, - modified: { $set: false }, - }) + return defaultUIState case 'set-modified': return update(state, { modified: { $set: action.status }, @@ -146,6 +154,55 @@ export function ui(state = { dialog: {}, filePane: false }, action) { return update(state, { modified: { $set: true }, }) + case 'restore-state': + return update(state, { + fileName: { $set: action.fileName }, + }) + case 'on-pick': + + console.log(action.obj.userData.type) + const idx = state.selectedList.indexOf(action.obj) + + const setNeedsUpdate = action.obj.userData.type == 'mesh' || action.obj.userData.type == 'sketch' + + if (idx == -1) { + return update(state, { + selectedList: { $push: [action.obj] }, + // selectedSet: { [action.obj.name]: { $set: true } } + selectedSet: (curr) => setNeedsUpdate ? { ...curr, [action.obj.name]: true } : curr + }) + + } else { + + if (action.obj.userData.type != 'selpoint') { + return update(state, { + selectedList: { $splice: [[idx, 1]] }, + // selectedSet: { [action.obj.name]: { $set: false } } + selectedSet: (curr) => setNeedsUpdate ? { ...curr, [action.obj.name]: false } : curr + }) + } else { + return state + } + + } + + case 'clear-selection': + if (state.selectedList.length) { + return update(state, { + selectedList: { $set: [] }, + selectedSet: { $set: {} } + }) + } else { + return state + } + + case 'set-mode': + + + return update(state, { + mode: { $set: action.mode } + }) + default: return state } diff --git a/src/react/tree.jsx b/src/react/tree.jsx index 5fb2af0..3b2aed3 100644 --- a/src/react/tree.jsx +++ b/src/react/tree.jsx @@ -2,23 +2,28 @@ import React, { useReducer, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux' -import { MdVisibilityOff, MdVisibility, MdDelete } from 'react-icons/md' -import { FaCube, FaEdit } from 'react-icons/fa' +import { MdEdit, MdVisibility, MdDelete } from 'react-icons/md' +import { FaCube, FaDrawPolygon } from 'react-icons/fa' import { sce } from './app' export const Tree = () => { const treeEntries = useSelector(state => state.treeEntries) const fileHandle = useSelector(state => state.ui.fileHandle) + const fileName = useSelector(state => state.ui.fileName) - return
+ return
- {fileHandle ? fileHandle.name.replace(/\.[^/.]+$/, "") : 'Untitled'} + { + (fileHandle ? fileHandle.name : fileName).replace(/\.[^/.]+$/, "") + } +
+
+ {treeEntries.allIds.map((entId, idx) => ( + + ))}
- {treeEntries.allIds.map((entId, idx) => ( - - ))}
} @@ -26,147 +31,145 @@ export const Tree = () => { const treeIcons = { 'mesh': FaCube, - 'sketch': FaEdit + 'sketch': FaDrawPolygon } const TreeEntry = ({ entId }) => { - - - const treeEntriesById = useSelector(state => state.treeEntries.byId) const dispatch = useDispatch() + + const entry = useSelector(state => state.treeEntries.byId[entId]) const visible = useSelector(state => state.treeEntries.visible[entId]) + const selected = useSelector(state => state.ui.selectedSet[entId]) + const activeId = useSelector(state => state.ui.dialog.target) - let obj3d, sketch; + // console.log(entId) - if (treeEntriesById[entId].obj3d) { - obj3d = treeEntriesById[entId].obj3d - sketch = treeEntriesById[entId] + let obj, sketch; + + if (entry.obj3d) { + obj = entry.obj3d + sketch = entry } else { - obj3d = treeEntriesById[entId] + obj = entry } - let Icon = treeIcons[obj3d.userData.type] + let Icon = treeIcons[obj.userData.type] const [_, forceUpdate] = useReducer(x => x + 1, 0); const [mouseOn, setMouseOn] = useState(false) - return
{ - if (obj3d.userData.type == 'sketch') { - if (sce.activeSketch) { - dispatch({ type: 'finish-sketch' }) - sce.activeSketch.deactivate() - } - - - sketch.activate() - dispatch({ type: 'set-active-sketch', sketch }) - - sce.clearSelection() - sce.activeSketch = sketch; - dispatch({ type: 'set-dialog', action: 'sketch' }) - sce.render() - } else if (obj3d.userData.featureInfo.length == 2) { - dispatch({ type: 'set-dialog', action: 'extrude-edit', target: treeEntriesById[entId] }) + const edit = (e) => { + e.stopPropagation() + if (obj.userData.type == 'sketch') { + if (sce.activeSketch) { + dispatch({ type: 'finish-sketch' }) + sce.activeSketch.deactivate() } - }} - onPointerEnter={() => { + + sketch.activate() + dispatch({ type: 'set-active-sketch', sketch }) + + sce.clearSelection() + sce.activeSketch = sketch; + dispatch({ type: 'set-dialog', action: 'sketch', target: obj.name }) + sce.render() + } else if (obj.userData.featureInfo.length == 2) { + dispatch({ type: 'set-dialog', action: 'extrude-edit', target: obj.name }) + } + + } + + const del = (e) => { + e.stopPropagation() + dispatch({ type: 'delete-node', id: entId }) + sce.render() + } + + const toggleVis = (e) => { + e.stopPropagation() + dispatch({ type: "set-entry-visibility", obj: { [entId]: !visible } }) + obj.visible = !visible; + if (obj.userData.type == 'mesh') { + obj.traverse((e) => visible ? e.layers.disable(1) : e.layers.enable(1)) + } + + sce.render() + forceUpdate() + } + + const mouseHandlers = { + onPointerEnter: () => { if (mouseOn) return setMouseOn(true) - if (obj3d.userData.type == 'sketch') { - obj3d.visible = true + if (obj.userData.type == 'sketch') { + obj.visible = true } - sce.setHover(obj3d, 1) + sce.setHover(obj, 1) sce.render() - }} - onPointerLeave={() => { + }, + onPointerLeave: () => { if (!mouseOn) return setMouseOn(false) - if (obj3d.userData.type == 'sketch' - && !sce.selected.includes(obj3d) + if (obj.userData.type == 'sketch' + && !sce.selected.includes(obj) && !visible ) { - obj3d.visible = false + obj.visible = false } - if (sce.selected.includes(obj3d)) return + if (sce.selected.includes(obj)) return - sce.setHover(obj3d, 0) + sce.setHover(obj, 0) sce.render() - }} - onClick={() => { - const idx = sce.selected.indexOf(obj3d) - + }, + onClick: () => { + const idx = sce.selected.indexOf(obj) if (idx == -1) { - sce.selected.push(obj3d) - sce.setHover(obj3d, 1) + sce.setHover(obj, 1) } else { sce.setHover(sce.selected[idx], 0) - sce.selected.splice(idx, 1) } + + dispatch({ type: 'on-pick', obj }) + + sce.render() - }} - - tooltip={obj3d.name[0] != '(' ? "double click to edit" : undefined} - // tooltip= {obj3d.userData.name} + } + } + return
- -
+ +
{entId}
- - { - dispatch({ type: 'delete-node', id: entId }) - sce.render() - e.stopPropagation() - }} - /> - { - visible ? - { - e.stopPropagation() - console.log('hide') - dispatch({ type: "set-entry-visibility", obj: { [entId]: false } }) - obj3d.visible = false; - if (obj3d.userData.type == 'mesh') { - obj3d.traverse((e) => e.layers.disable(1)) - } - - sce.render() - forceUpdate() - }} - /> - : - { - e.stopPropagation() - console.log('show') - obj3d.visible = true; - dispatch({ type: "set-entry-visibility", obj: { [entId]: true } }) - if (obj3d.userData.type == 'mesh') { - obj3d.traverse((e) => { - e.layers.enable(1) - }) - } - sce.render() - forceUpdate() - }} - /> + mouseOn && obj.name[0] != '(' && + } + { + mouseOn && + } + { + (mouseOn || visible) && }
-
} \ No newline at end of file diff --git a/src/shared.js b/src/shared.js index 540f232..8cbaca7 100644 --- a/src/shared.js +++ b/src/shared.js @@ -30,11 +30,11 @@ const color = { const hoverColor = { emissive: 0x343407, - point: 0x00ff00, + point: 0x10B981, selpoint: 0xff0000, - line: 0x00ff00, + line: 0x10B981, mesh: 0xFAB601, - dimension: 0x00ff00, + dimension: 0x10B981, plane: 0xffff00, planeBorder: 0x919100, @@ -105,7 +105,7 @@ async function awaitSelection(...criteria) { const counter = {} - let references = this.selected.slice() + let references = (this.selected || this.scene.selected).slice() for (let ob of references) { const type = ob.userData.type @@ -149,7 +149,7 @@ async function awaitSelection(...criteria) { window.removeEventListener('keydown', onKey) } - // console.log('fail') + console.log('fail') return null } diff --git a/static/favicon.ico b/static/favicon.ico index 30805fe..95bae8c 100644 Binary files a/static/favicon.ico and b/static/favicon.ico differ diff --git a/static/icon-192.png b/static/icon-192.png index 017a82d..9fc508f 100644 Binary files a/static/icon-192.png and b/static/icon-192.png differ diff --git a/static/icon-512.png b/static/icon-512.png index efefec7..c3d9bd6 100644 Binary files a/static/icon-512.png and b/static/icon-512.png differ