From c09b6445d357eb0a87eeee8fc6698b04fcad7253 Mon Sep 17 00:00:00 2001 From: howard Date: Sun, 25 Apr 2021 23:04:24 -0700 Subject: [PATCH] major --- extlib/fs/directory-open.mjs | 32 +++ extlib/fs/file-open.mjs | 32 +++ extlib/fs/file-save.mjs | 32 +++ extlib/fs/fs-access-legacy/directory-open.mjs | 50 +++++ extlib/fs/fs-access-legacy/file-open.mjs | 43 ++++ extlib/fs/fs-access-legacy/file-save.mjs | 40 ++++ extlib/fs/fs-access/directory-open.mjs | 48 ++++ extlib/fs/fs-access/file-open.mjs | 51 +++++ extlib/fs/fs-access/file-save.mjs | 67 ++++++ extlib/fs/index.js | 24 ++ extlib/fs/legacy/directory-open.mjs | 58 +++++ extlib/fs/legacy/file-open.mjs | 56 +++++ extlib/fs/legacy/file-save.mjs | 32 +++ extlib/fs/supported.mjs | 43 ++++ icon/icon-16.png | Bin 569 -> 550 bytes icon/icon-24.png | Bin 793 -> 743 bytes icon/icon-32.png | Bin 1014 -> 937 bytes icon/icon-64.png | Bin 1791 -> 1762 bytes icon/icon2.svg | 10 +- icon/svg-to-favicon | 6 +- icon/svgr_raw/coincident_alt.svg | 135 ++++++++++++ icon/svgr_raw/logo.svg | 10 +- src/Scene.js | 39 ++-- src/Sketch.js | 180 ++++++++------- src/constraintEvents.js | 30 +-- src/drawDimension.js | 2 +- src/drawEvents.js | 45 ++-- src/mouseEvents.js | 132 +++++------ src/react/app.css | 22 +- src/react/app.jsx | 2 +- src/react/dialog.jsx | 40 ++-- src/react/dropDown.jsx | 4 +- src/react/fileHelpers.js | 56 +++-- src/react/icons.jsx | 135 +++++++++++- src/react/navBar.jsx | 53 +++-- src/react/reducer.js | 73 ++++++- src/react/tree.jsx | 205 +++++++++--------- src/shared.js | 10 +- static/favicon.ico | Bin 24838 -> 24838 bytes static/icon-192.png | Bin 4965 -> 4794 bytes static/icon-512.png | Bin 14298 -> 13756 bytes 41 files changed, 1407 insertions(+), 390 deletions(-) create mode 100644 extlib/fs/directory-open.mjs create mode 100644 extlib/fs/file-open.mjs create mode 100644 extlib/fs/file-save.mjs create mode 100644 extlib/fs/fs-access-legacy/directory-open.mjs create mode 100644 extlib/fs/fs-access-legacy/file-open.mjs create mode 100644 extlib/fs/fs-access-legacy/file-save.mjs create mode 100644 extlib/fs/fs-access/directory-open.mjs create mode 100644 extlib/fs/fs-access/file-open.mjs create mode 100644 extlib/fs/fs-access/file-save.mjs create mode 100644 extlib/fs/index.js create mode 100644 extlib/fs/legacy/directory-open.mjs create mode 100644 extlib/fs/legacy/file-open.mjs create mode 100644 extlib/fs/legacy/file-save.mjs create mode 100644 extlib/fs/supported.mjs create mode 100644 icon/svgr_raw/coincident_alt.svg 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 fb1133a228fa863e639c0f4026f2da7c480da136..fadd0ed7cd20a347589dfb8fbaaf53bf1aceabb5 100644 GIT binary patch delta 466 zcmV;@0WJQy1f~R#UVpPmL_t(Ijg^xzYg2I)#ee6Y=(B2QLWF9-P0-Rw+pSbVXoH}F zA3z)g7r_>EP>|#$P(gHPC&A5KCl#89Lvc_zg}N0 zjRR=p2Oc1%fW?21{st-p-5e?7jJ)G*KGL( z=>7wmHBkkSn0BEi>t{-@;oy$f-k|P4(;G1 z?nMxx7y}BrSZNw4npqU1gVZLzymya7O{hs;TmSK% z|NrlQ?>Pr{f%j=9J@I;RXg7`|6$&5b&fz%MRn=7^OYd{VSAU4;o(B2q{0PD|QF8wj zl)6H>GFI9cXfVF9kZDHol(Aey)C`QJqKaLyQR>O6Km84$OAN!`hXp;ECRV6}J zlf8+^Q$?CUsDGAcsm;wBhkgvL6l%eg#5{IHJOHnF!=x7EmQ@%PsVpNJaECXoF6-9&IJxDaz&Wd>TnG`_XyA>h> bdI7;4ZD$;>y7ng(e_i*ivdiWapA!e$&JXc4XYMG#cbHp=qN3?+@ecUm+vPU(y@ zYMZ{Rch5cd^F8-I?z!-fC50wd-yf~A%jeFhAtfT^q1ncs*nj#MsEQ{NYE#isf!b8` zD?-S!{8wmgwO4K6dN{CC480_zv2FVt%E6V2NMRl*(isc% zeavf6_jIfx5l?iA&Ghxc;FB~tl6Ble6{t8w4==A0e=UA zmhIh^hI=Ey7I!2#m`*57e>h5=h@S9F4IIXy+uG6oDh3m-uzAmbevl5JB$ciRw>He{ z$uIlNPXNAX$iLv|n4--nca|7rq`C*;xo0YPdqMShJbz$*2D*IF&{fBF##QhdhIV~x zG@k*u!saGJ=(ioGLlz4lw?X>ol(tf&#T^cg#Ns;swXyFl(2S}eqC7TB-AUt!s15gb}w}Ub>>e001KDiUtGjq`VgN8j>4ch{10r_zi~}nX$u70e>ND3o=-X z{v!hsoPT_YTEyFt&05?+nI08k6!^L9BG3ZZ^MX#;W!jw%WbT2r*R+71s%i6jiEUf} z-Ct6I=h$q(e3Knra?uhE<~yLJh=%uGh2B`x*(Hq*^8Dli8lI&}k7aweFV{tj vQsv}ujVEG;(qm7=3{_4J*Q^P;Uit-8oa2gloep1V00000NkvXXu0mjf4)8ya delta 711 zcmV;&0yzEW1(^nrUVo-ZL_t(YiQSalOVeQ#$G_+N)^KU2=~ZA*VN|*%a|+c>l6Tfk zpn0Q4OU+&o`U`~aqav~S5?NiyG$Ja%cwnJed95uwpL;YBl>a>@O zzE{8J{LcA)o)^vm{<0|5#NET^409QV!%P}Wj`TP;HqQPTfPa9@V{K*3S{)$S03d=H zX069j@aZ0?MXef8^y+fHE==_RwkK!HBr|t73K~2s0+hbKl%s-cY602@l4VaR0*8*c z{h6`;+me6+k6mvN??Dk%5yY^Zah&Ew;f~(tvp?B$#vciCY{ptlZGcGOD$D^u4~@12 z($;6k29saWcz>5ksZc!#n^LkCdQ~5fWWsTp!%Ga#vlM#M%zkBqiqi3@f!f3i;@&_iTRMlZQIX^JA0|>9f1D5 zgI&Y^tZ+3eRDgsRoCT19c?n;2?`r%G0446$JW)C;7=LC3dK~f9AyGy}>bzylHX!M^ zGPKn?4WOgM-Fjc3vkH(w66zQ+op=KPdk4GL`Ln{+qEG=AjyFpN00;!2jAl`48E@~) zn99FBU@ZFxpalW~K`QNbZDOalJfOlgERwlcMx@Kx7NM0(nDFJ>*{}X_*`mvkns(yVgC>Tn!iNZ|)!DKa( zg0Bd|5cKQxfzC)Q+6QB%S|5OJ(`eg69rIBll&oaaBH~h^Q0GhYsrRw*ghq1}HGKwf zb_E0P3rM@}G$sx@V2MY5UTAe~HfA>~qE-?5kw7HH2cmA7!|7yGbkP&Us+X$50Q!qZ tY){2N2O@FBctC#Kby^yxm#Y6K`UyZN22_2fw+H|L002ovPDHLkV1g&INcI2# diff --git a/icon/icon-32.png b/icon/icon-32.png index 128e66b0230b3aa68f0eafe3d0a3102dea4fb412..b1837e03d33a148dfe8e33bf8f8f058e9c014298 100644 GIT binary patch delta 856 zcmV-e1E>7<2dM{;UVk=8L_t(oh3%G0XjEqu$A9NejG{^A#`u7uLMa7pwW}gl=^{!d zqtcZ%t5!GN=%O8i!4ysAn~7E>f~GA=zn$ZIp?1L?|v^nc$GE%lzFT5Omxw+^nZ|AKJuU3*=jXD+@i`R zpaJ1XT6OdNPILG90vKc2ORSF&9Ry|-WRt4wa#OmOF7vSN0Cp_Wj<64S=c$FtRe|nA zF#JPZ0H!@!Wz0dV$Fk7=Nb)J(zA}>u9^VSJXpTn+$~C zE)C#Bt^uzDw-hn%`RoCV$>#Dc!vk;nG`mA#ZAok8M-dGrI?W&Bv7$wNE!i1=tRN#9}`+AbAw_P8L z{`SozJAZzAqpD1D-Rzg%<9Eiec`c)a75RkXomMqKP7i>=rsn3|HspS)+^ku*;cuS z5K6iW7MC)If)U7*h$i9iY-jLz4VTkLp!=;XeevM@Bi|D~NU{0^w7Y_N-m!OhcPv=I379 i95bJDrsAJ@sq`Nt{x|evi1>W~0000V)x-&OdHg4@(uxD=UZf%(W!F!s43vdw@V#%IRs#?A53V0VHD~8kk z!3#-O=s9D|YXE-RFql{(t>_(J?s(b`w=;l>E8xqFg?|l^;x##&^p%>X)R5*NfGul+ z0ZqXtR{QlcUvxjSSUHD?elY1CIZ;)>n~6jT+#=u=A>K$i10REwVtpVrzDqflkC5D7 z4NhUYP{_w-Jtx1j8I?7)2!BfU27_w~lxE~NnkF{u9Ld?o4V)kCv!I3{QhX2m09t2e zT*IH)$A5XUvAdMfK>+uz_-sf30E8kipD&A?p6@&UbMf?$#I0|$i;rqCj%}B_C+y4b z1KHTlWvvebco-~aR0=q-UMM8EzYWUgJ0E@TFK&M~>pYZyn};-j3H#6klar3CT?>!o zjd_AZ`?_nT8XG{hAyDy7~^xK?k)imBa%grxe8f7)?*O9)iw0OsolLmP4ec?l+5 z!xOdOKpiimDgV)H0M2=Hktr19$y$4X5?s=XdMx2OvF3KXZh>NwJ)s`~yyD43dzHxW zDlmA-z>LrK*7rfuREFYlYi~NbyZC!=Iv=`wC?2;q9lRO&8=7r&ILk)ne*gdg07*qo IM6N<$g7!Jd=l}o! diff --git a/icon/icon-64.png b/icon/icon-64.png index ba62b6a69770b93f7cf3408c151c013285abaa78..10a782f5a3682daa5225e3e7615121353720681b 100644 GIT binary patch delta 1688 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 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 30805fed05522f24e27646fe88d7e5972e171529..95bae8c29cdec0e21570e5a9695978d2a32a8266 100644 GIT binary patch literal 24838 zcmeHPS&SS<8LnXrLx{cIy|&2`jCVN|A|Qc;5Y92-3<%zMMdAhV1TP4ar>&WFg2N5s z#SaJxMJO@|DRQzqJ-cDY7-Ap}C>)6~U8p6wsfKC8VXOz7{$e=KU}|F5 zBGG&$F3eGPAwy{=UAHao-j6n3k}^@A%H(}fNb?W15$3#qqP-rGbfWR{ZyF0s7Tw#-)p`$;_s-XZHsh_?F9aQeCA4|#x= zq$}Z+e%y}(vW2vZgv0IQJk(g7d0$)?d9?9E#`eljUyM^&x#X_BC}lMasy}l?&lk8ZpoxD}N9_Ih@+=||#V*7A5YMd{{KIK&}?~c&yvAre$S%Re%g77mxMX(e?i8*RQ%At z&eeWJ`~OprkCU&f@>TP+U!wUI^(fA#9k(dAogXXp$R>ICKSF!VWQ>pYAgT|L zpo{&dyOxZ_&^k(cB-H&=RNu_M<^GoToXK^2-~$+AIBHuzql?wVG}`WOlJ=DM>b{uD zNPp_DB|ujwyEcqMVH_jPf9@wfV&mNtpsL74Q-DyMfI=ts2YPUB>M7&#?;1H$F*6(5Hg!LDx8p@I!z-OnDZb-T6N1ztIQk+H))#_Ai4Ig}_LFoY#K)gb8Bf?#t#2>OcwU9^cmgv62J zI0XvM;{6h(={2>$)B=;W0L4jp%l#6~vq{cWM$dLqEHx=((R*cm6aJ>|ePigK317h4 zrztAeB=DO4%$wkM{zP5ZGxV4K&o4%O7cdF^OgylLABg`F5{NJDE)YU6P~4!eOL<%=r8FMKBH4quRM5&DO?wx z!^^jq8A1Qh4mjs$bH*u;4n^vu&k;^Y|8UPJ;#b4Yh!xQ(4~~qHVQePOK~7YEIVMu) zEc574v`!Qc{k=M$+2%*?|N5B0pVjjeMfEe`(Yw%5&N`?z8k7FAKm35Nc&{_*VOy@8 zmw86jVU+xs^(Q-w9J`3mBl+$>M$uJ=I|2P=U$}^KUWb!${5*XCe%E((vX@b&I}57# z|5EnSAnB4vXSEJ+?kz)C9gPXLe`rIzzfg6{lb`B8ZGTQ@;`%!eBTjmAmVSAk=RTuknhPeUzxHgPNl!XcN$*Zf&((6}9kT9>)S$2H zY%qTP#o0_jliybjl~ZXm z@;~zSE9HIMhL(Og`ipl8x2|1&g-*|8SxIZS)x2W-ZH>|O-|`K0Qdjk!e7Nvzx$+w( zO1o;?C;RzD^zqc0bCs`}r{5g`pOZ1K;`(d92~l~e`8?mZ#WH2o4u2{idoc-HmI@&e5OjAkG9OziAuQ2GMt3df#Ga=`Z`jlbG{A zHmcqdKhwY3SvGizzG30(`*%zE%Yr+74A8n*cHh_8ZU@eR59q!p)yZCFS+oZJu?(N4 zAdQKxeLCB1lMl45zK_lhlk$OWd|#cM<1QQD9>_aeW=wUQb|`rdY_<0tr(U+SM~s13 z#Xf9hn`C|47?@4$Ym$15$}2|<)GGElsyy3)Z-{|g#lE*g?y`B3FO5CQH}GB4H}m{{ zLfQPM^rjY=T7Z5$z<6Ca9X?t*8NzBuz+Saer_4sR;}Gmr>E{Q6fLA*efIh2s*dl(5 zfZ#l;5k$ofK~!V}S%BXzAXwi9SnGy-=Ql&R*cTAObCRdpU+fTcHs?18*1I+Q>oNM3 z1;7gZ;sUStP>4tM9x9<~y;ld-TEE@{5mpK6umdjG0o7P&2S8y5K-hr;pkfTWbWgwG uKoE8l`1ODc7PeAHppij|jHozI5ET~*2As@~s|zZ0ZHQ`+pe}Cp|NaLL`2E-b literal 24838 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 U>c=$z7P@wvYT%&k+^YZmAEA5E9RL6T diff --git a/static/icon-192.png b/static/icon-192.png index 017a82d494b9af0d5079fe3ba269d911de4ed4dd..9fc508fdf7611ff6c42336b10e36e9f402138912 100644 GIT binary patch literal 4794 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 literal 4965 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 diff --git a/static/icon-512.png b/static/icon-512.png index efefec7751dc6648fab342278edf8ceb375ad97e..c3d9bd6e35ddde21df3fe7f4b29180d932d250af 100644 GIT binary patch literal 13756 zcmd6OcR1C3{P!235DjG>lo6TPIfNFnW!&~YRz}9LIV~s2$Ub(3LdbS(9g@uKEk!z< z>^+~)sqXvkexB=k{`y_d^>kfDe0{&4@8|O#uh;AS5~`)4bexKr3WA{HD$3V&Acz!v zN(vpN03X(T`gXvFV{XcZ9uP!}BmO06aeZwAzR2Wx<1f$Kt_V+WOLtqy+uK{n!NtkL z+S1Kd$kp90c|n#Lf;b?R>x#NQDL=-YVpD8W7Y^q)b*qjmSLL2&JmKXjo52(w;iJIl z#gpjib?UEz+W2p0r2XLq9n5>l3l>*Jx$t$>F((o&dK66??od|nx;{KSY(AmleflZ- zZOu(kL%SRhIuMWACXC2sLD&5-SrLQ8Z>IED;!(z^H2e+{U7aW<2!i8v+;~(Cqfch)`C#6GJ$UM{JD~BCL58_ z?!8;wY|*13?st0=Y`%%5(kYnGLmbWv)j3%rD<5Xu*Hs;p@0l;Nh%O3wKVJ`L_V07nWhNcakf`wJe|*GIs&JMQ=`7VgrM+`<5bs@g}qjt)MfaQDW~nKP0Mq2#@4EbR3KS) za>!o6*?(u!IdoAJzc*y4(UUkW)#*DI8y*>KKYlP|39;TFftWwsudb*mHp+L2nLm{^ zf0#~J{ie{}^~WmJ^Rr-~p8F`0q=CGJ;kzFXKJDkVmumMo6s@+u*gfStsC;ScH2i3k z7dVmP*1=6PVSHIpNndEPXZ0!b+aN4Xz-glNz&E4cneH!Ybx?8zavU~(jLi+& z-}SkOWaMxH8c4*9#OsYxv8KJBSgpfq#+YP0+}|Y>cI3k;Do9IM$gk>NAwy;paMBmX23dJShY=wVDp<-wb@EA5t`Ai4q5gSd#HpnEuY#K($j@=D?6|Qk+VC?DmM1Q`!DKlL4rtG<11Ok08<{Aln>(0QBc0c8X8baMG_DuKLB?rBt4@8lyLoWS}=>o#k zhcNiDZZ+`zV;|_y-k4D#6^5qH`*yxPYJ*)4%Yu=Dw3NiBlYkAl-;2f#>lqZbT@m67 zO{HvESHha=3AyYV%BDRz2rxuwFnGeK}Co;6b z1z!_W@UW=0sv$!WL80CRmFbR^>3lsS!<6!&8pC zl7i;zo&*tvmtcotMksK5O?_=@u8#Zf)eZDy{9MCeN9>Q#Ll(a?Xu;T4Mh?l^E;=|K zU(%nIB_|&aBYzx9ic)K%3*knT-|jKCwo^1!Pu%;q>^T3PtgX;b00a*O6(pMD?6b<^ zVKzF~qhw{LsLLt3o?yJ^VZ_Pfw8Fp@4trYH=TZWd zq}MIN0(;b0xKq!<>5?WkJKp5=eOmo7q=>>?CsD{PrCT>8WOsd7PZ~0ey6DpLBohtY#k?d z3nXI)iWC)+Fj<>FZj?qFZ7SrTL6B_-bKn2iF0XenWW4AlBpgDDdhcbEZBTckhpFf* zbm9F!HBy7L<^&bQtP{j*VqXk$2i)83)O+rF#r!!zVcHpxoz~h=w-$&T7I%b!SZ`SD zWm&k1*_L6xi6PkfDDcK7>}Z{Xw|iwu2eQ0)I>J*a6S?K1Yw}EJCZEENWI!B3-uyJq zb$uO$68JYmcTqcU$Aygb_*`~O30xn9sGt`PqrHbNssRZqvn`#Z*OxLe1zmENz30EO z8&$|78ExpuA+L(h3xb7j^z%j&@#I`8pfW}dTlTTY2WU$bR@t@i?J8{+iuO{dtXj>I zFh9ZjtMpghj9(vPfm0QOL#s**2wW6W2|fd-?rs>%9973^hSN)3VT9B4gp;BS^(ylY zVY%C=OuD8{Qt@XGq6_#db|j2g)1JU6@YhKi-XvCR&XHs1{-P7BZBX&ma$#vpm8)o? zy}$MxvOvI6@jqf>BVm-vp@I6V3q8e4F+++`Z1hlj;-1A{RtU1_iBoXusH+kfm()c2 z?Y?_626CXDCxNbT!FgiBO``w-pEf!PE+OIeHZ^8iPc>qCZ-rJ|eF0gFQ0-oAeD7Ra zt*`0`w)MlQm>|AY{#&rA&LP9@6kbUB?m4bUn>-000;)6XAHyg=NRB4!I+=2|;e@R& zD0)1HK0l=2cvx>>1s1^uT9w_?KZ~NGM+RhZ>l>Z3uUAK|@l2IC@+{P{{!lF$1ADKl z1uebktm91ad);4Plg(g%QC_5CLC3DE!irMy>kH`fBdqa3!2we>;W8kN}P=fO1mJ>I$`TV7JVQI?t9@!6y}Q9tL6&Xa%tAbFpBU8q$sz-uF2RMH^!GB@)-19Jpc1Dt+&C^dFDFC1ZfD-?;X=reKS7f z5Cqm;ZAjF`J=6Fii8?8@Wmw+{+>y2Q8JtO+iK~*EFr--s#k$66o=_ zC4yU7U~+t+pA3?*kAa-S`d2yRAD)T+*3?Osac*x!o#i5|wxXH#K7d&^&2=IdYVkTuj1F=ZbsH=7a>Q~{B!^|r4+_eH= z4?5biDBTzGlw@!g1c5|DdX)HOnciLD=+37wN`<5$m#+S*i>mKjtuHjTXF!?PM=g9J zF+W7{wcrJn^Dni!s^yM|7t20nPa}dXB#OTxHZSD0763<{NRg&eIGqHtXFb}C1q4vL zM~sWX7pGgQICd*^W1lvZq#PuY;zSH`b~rU8+j352v@Jg<{4Dwv_`d2(Nc^6!f1z1a zUc|cc#xWJ_kT3N4hAeMIIH-HZR+Nol;ELjZ>SAE@3aCvDv6w(p39kgWe-JC2%8U^J z$r(!ADU7LW&pWWq7DvXEyqwvU!uq}b z??h^}q(@Oa1I1C!0cJc~&>wANJxhXOn%#-R1n26~R9h>NkaN_%EL^PB_*oho2qm?l zdH$Itv$I7pHcaFkFWy;9T45--Y6Zc)Uzgl%b$x5&qUnI&xkV5$yiW4i;-EX;VDL?q zUV*B$9SQ0~T9ktawq~j1Ctg>J*MjE0w?F?O9BhOhs4DHu#1^eW3OKiYt#9KqR$B-H zuc0EzYfA67$^Ju2k3gfIJnI$`)O*{10c1}EsYu(%##;U8sY%juG*rPna9X2DAYUG7NXY-h>!SKsb=oNw8_tIe3C5`#&et(63Y-_J-F#o zO$3i;A!RJ6lh(+sX>CY@RIC;{(cwDgsh9O~vwTP~=?uAr3@Fqn3QO}6BWEuM!z~p8 zS4Rk`+x?XXmZ6yA%6_T@0SHp*dD!a8R#@&L`*tt4#R~0kmLA2IR3ViTlgE=DzxM*1 zHYEI->$W+zS^ibC<#}Y{&4eb}ZkkTkk%JWz3G6)_JH%{MZIZmIIf}NTbf6f6QlPqT zl+f|8!eeP1R1L?z+>nR5Eqt`QOSPbEhsJSZGsArSisf8$Pz@}x`(lMuX>7CwF-=2s zP#?@%Q4B>Ya(XOZq;y+Hs(v7|#0nh6nw5}Gwiblw##|dUKPaWm_i{*8@gT+!{r0nI z)1PO^;h;#8NbdY#>iFf7JkuRz@N(VL|BZtra0~4}dMqT4`A;M{Tqj9%-hG#{_t(m$ z7}sZ>H$lDW-n!%ZWsVI`*!CrGIou?99kaLK{9|CnGm7A@;sIJ6D&*_GVcbeeMFjAF zW`g|*VkYF2(b;e3zOqKQQc)>zZ8yHXju=cX6#<9%IFqM0|MY?rEj=zBe4Cj{L2Yxq zPlwTW+8w*YUDg~y26?R=$9l#6%td8=ZHT#{CU9zPEKySBcRE{~mE)>7UQ;J^g-U^} z^Z-7&Y+MD8ANw2RAh!f80}pTz962_9S)q&;1P``n}r>2S!8Z7Oh!XEL7qolEvOc>%TrbL4`PN>X|R!V*#1W7 zPLD|1GbStXxwn)Sor@k=eA9w-92-rO83uC8Qe7^-2hGF*7VBw>9jOb)OaLH~X|k&| z*(?N}YYLv56wj9>^e6@4WvWKs?o7RcDwMs}K70XYRN$FK636Z%123z#YQFNKt%5`V&7SgguY}fz@>z z;PYdf5|mG-y1?_7wIMgFf0YoGk1Y{o3elxqjzE(+8*~bOG>MWN>J@ce?5B3`FJc3n z^EF%S=zDj&F2SM)8QYoJ&@_5yldB-lx9CRTnT5GvwBr>HJQ*m%Ozu9}75#;uzAWj2 zX7fpsaU$lgm9819uEWQvwmjem=|gtdo_X>VmOr(2M{fD46o#jgD_@erBf0CcOU3uj zArs&H<+d4bpr6<8>~8v{=VhGLBEOYg(lI^(D4Mb2@fW>Kg0$`7_X9n|2#13iK~^C* zu`Z#~JCYtJSM<`R`I3CE8IWm?qb`?{{6{@2Bd0LK8-1c#E%m4D;{Hws%i(klgHxJ*b4cf0PX(sovKJCJi z_!KPiNX)Yt4tTiAiP@TtrX6@2f2mfJkw>1*I2NuF^fl#`#_-k_df*4>VEii&rcA_G zqSg)=c4i%_NIu*miQ-DYw+Vg?nz818$GQG7&UZndaU>uo+1#ne5QJezBeMFAUREXE z8+7qz-TL9{B=!ooc_F2xwL3@t<}2~TmY=u&5OY)!DW9v=dvu|m9h!zPUu@>YyTpBQ zkkBB9iobmNGV)-?%GGW22G2MkkKGnPC@RoBEA(ZlFdaaBV%b!3KhFC=ShDTQ@dl5f zK>=-V2Zt_AJ6ZY&etCK!@nKZv4REL@!;|-@Pv?)k4ZiCm3l1o;!frJ=y2(2+U_11n z8oXTw#42gIlH8J)&q60qKUailf5cEE+};|=%`UcmR>iIL3Sq+{*$_qJ#IR(eEBTTK$0*J$zB=5OCH7GK>Au!CgKVnw7keK5Wh) zj94jq2j{N6f;?+M(PvZTC5tK9_#Aya@;z#vx7_*D39P1P|BMwyuH`k|(J>|_uKMd_CghHZ1>S*it+aJzf6%C$m_-MeuvwtJ=Z~esX}iK^yQI7X*5q z{~tLNoRIJr(7IlABzyn(8QdWCw7Ui)ZbQskFzN;_aCPUu@+uFK^)ustgaVu9BC*=s zUf-J_;?{+s=*(+~9hwr{`p@q6w`h9`PA9s!8}9=mWGiw=vH1MAHJ>CZ$ec{}@+(oe zNidx`D{Wj@?YW$t%k8wBFXXX=;+5J1Hrm4-ObUK~ds@xalIC@g!3qN%Revl?r+3y0gO|@jjg_ zGy2Cyu9rc)>rUa2O0iY0Snc{%u=eA+P=qw#`p%QtK#q+7S=c$mpuxgkA`v~H>Ssuh zRi;c}Q_keS)oeDQYbbm{y4P6P;UXkUCgM-qMLT;--WpjML~InXVYSZ@$4YRC$G9<7 z{P}f{arHsPSHRU{P+!=*OWUyH&jVnLPaM`zaRn7j{n4ZDqu+iUpPhCrzgBRjsDxEB z$pzwt)|=_#7(t=sK5Tk<*!G*2*?VqCsP?HF7N%no@%an@hB!u(x-~sk+i#~GG#Ir= zOi%Px2IE#G(i`q_)q?tnRPLESc`C8W|G{gR6Rj28!G{(@1N<>8kl zAA$&`vH$y>Q9Fr6Awg>SP#72g->{{Ms12l1D3aKV)1w$XeUO#^@JoH0t*O_;EC1;9 zTw*>$>&*ZeEYT=b!O~!zlZUe*4cM7+AZ3nbc%^e-6DGdvZ<0#}Jw>!daRIZqHj_8(Ybzk!*Mxz%wTBIodb z&sF~nM1Wtv99&~qzA88lBRRJuCAIW!L3P@cv)N6Pz+rgz_sEL7uqLgzj~=>0`XNof zRlnbTLm}w(VKD!Z1Dd|ZKe0se1#|v(`X42~pu~w(u7m+h&#NLI(PUG8=FEjv0X zsIsfQN);wI83E%`1<={Q{Z4|)Vt6WdF+lz)t9^Vd?1$Y|`$agL=4rw@DifgmtYcZ< zK$C$&ho_|frFy!13YRa*#^|^@q~Y^9sT5v5huR-si!M(v-e(r1)occhN(q7H&NrZ_ zbNbP9N{) z+{bq}SP$04F8BJ&i`DuR^zET$DJ=8@{*i#AxxuT^B{{t!LWK6HyD`D{q;3xk-mjJv zexv6EAJ=`F%5AIT0g88nswGl7B+ybLNMJj$wL)2j6Pbc9sm&g7Ea#hhOP=`sYmvP+){E)lJBt>0F*B}_{*n5+h5{;3cad2 z$!8`=O|MaHkUqLca|BHqX5l3|!gLb^Ph5u^|4;2p&7XV2hPx>Ag#O zV?dwzgR~loZW~R3;U7D^&0|Sh`-n3xc`|WzL_gumDY$A#Ch;IpF^xFWORTA|g@nGd z@CcR3oR3vE!{jDIQ@QyQ@!?NMyX|=)?MrahM*IU~bM3O_6U$jbKvutot;DN}e(it1 z?&g`j`IF9R+{_}zJ%o5CR)TZ(USE!TDdSt?flI&D?1pKZ;LJ%YbiZPg?PP2eXo)SE zJqv0%FU{tm=~Te(!L*t%T3&-$3Q8m|AGOW9*sNY3mdYhICKzmRh*lpF8gxP|N)?YM zDIc(#F`3G*F47d{b`<5!R9)xW!HhS7lZi@_SEEOT;6A7xQOY*uHs!;|GVNsrTZDSY z0l_k{(fqWq)!9SiFVn>2JP9FG^K9~XzFlc1xWVKC?n>(lZG>db&9-+FmeNg-hS=gI zIS}#gHrboD8`LVbUzS<~rYN9wck}a$R`a!WRJi@%3!9)MdfY#ptDKhN(sO1uWV>m{ z$&w~agfcOUr^IT*1tdVff%;SmM>k)D+ToO%EG9q5|sGETQ zUTmjTTGE+>N#PTs^%I`db}YSb^puwA9X+w=*$@ks-YtF-2a?_V48^WIGh9tSmI3Xa z^U^y|V%9b0&gP{n87;CUmWw&l1SQB(@(=ClLroGopP8nx z6RNXC=iv3ZjIt1PMvmwOC=-3!OiP|p`a3tUu5o|OUHb|`Sv&~`0%D_gqp|J`801{w z1dzTFK>H|+nfQ4Zs5?<@wz9NZq_2x0lGG_CR@( zRd))0(oObn#n#uk7ED7Z60##9omBZx#nraK=E+>6Lax=vly0YvHh zR^t9mwE1M)RJvKtWhcNdqJpTWnR!dc4{P6PZ7C2$P+Vol*V6}s>c(+u!b!*3Niwz* zC(aE^QC$NdhBN72(Y=-j$!5U=!nLP)eZlCiT2@p51;HoWM!!U z;Dc`fbTwpuxSElXp(UJTayjGm4%bwcxkR>$C(8C6~^Lmv(J~oOn6mJ3_f;c=nzc!MdW}wD7QUz;BRwqBc}67Dy6-6z!8Ww z5)TtPL|DEKu5c{Vj8AxU7y&yb?EJS-+3FPvWYmZ-QmBTj9?X~C239WaTo$aGDQccl zAYAvUGZ-)V8oU2;*Ff(=E=#ix`%?7QQ7AF#_E?!qKzkz0Md8P_KnC`q`gE|OBVdMp zdi53p&7uS}dt=7A-E7rEA>b5Pg*qNaLMVJ<3QUM*^AGfzDS-Js`HHkjKLOsf)Qt>M zOKV5yD2_c*k_vKLiD;)i1mwAe;aJw+PTTeL*MKts-I~y5{feaL0V&ij zUEh6rH)vO@(zj-$YPn%0k1Z_qHPei@tCOZt%K&!p*I-PU1snmQg*9P+@iKPV{A|c> zRjII6`KyZ$fhFKj+lgs6t9pRAt}|fLjVx=E=)Qsg>|`rz=3nLtHX3mzn4e_s^U&~= ziB(?OUsd>nAq?{|v08@!|4ght3GUdzIj<9xs|ytaZ_{ zr{cZ9S13S~F^8CEHVNHfU)WVkLZBlt`0<~*3`7gIETbqTDSLPdY+|}*=bHN#+3|60%lW1buADMy7A#fBGZ=e;*;0+qe8u!m8Wuxm0E3cJ2;Popq7L*q6;hg zDfm}`LK}SgfrIGKV!BQOK^zZw!KXGS$RP+twAMgTiuB-L(Z~PqFIM0bTOB%?-IXj2 zj|%$V7m=|>55+V`K#=lf%`U^3|KHw7T+}-fW;xQHBn6Km`QO+6f8A|Btj@y0Im`&i z*{K6T0D=gosuWmBM8tNO^3t2K3qRVi8$;Y#%); zUO%`x^NNZl82h)U@T8GZ^w^rL%=gbNK84w2w}-+kvWes3DEg5QsmY|@u<#j{m`UdbX7C%Ni7)hVUFi)AN`EIm}=?j+h{mj zMG(cZMW#{$F!OQ2$h@GeK3|t6M|LlZegcwhs}^D@R6ZP$WOUKYG;6(bj*kI;7Ror+ z4}`0CfgSfcmu8175yn;24Vj-hMb76q0a@PlZ;TF}tMC)+v^4``M}&?r21t&wMV`I; z(K4gN@rT3DaWyG)eLZSSG8Qfws1l#=I-a?~gkrdTabrTeVCyQTiaY#$CmHm*`dakP z$NYnzLa`xyFYDR6;_wLDnHYWySAk#NEAXURVw(GC9<-n=!$SnvE!s7 z_~nSDx0!7#xT(X$(Vx8M7$m~KAXc*QYnbA2WG~!XCyR5;VBx!OeX{b1dUGHL~j&8!Cc;l+CjpuA-NGGk$ZWX3#?m zGHbvwpsz*0+j}l}s9!Rtj};0}JqEd*zf&>mXd1^oRZ$cpevzsnjFHbJK&wZI+W%e0 zZ!Mn`Vog`woECOj%kSX}p**`SNoIYa`7$yH8KFN2HJ4B?&> zw1<43DJ*iYzK+W>rrLd1KU$gZZBPMxc@d9dJzT#m^4Kl<_dmA!XYO)uJmItgiL~NO z^K$`*{hHdTZzdAkrPB<}okt+dbk%5kwQBQH$&hx^Npg#%4dG+;(VolGge6pFMH9G! zX_;s?r1u9tsZ0!5n@Um&EU0DL=@CD1jQa!@)n`ypE}ZM>wv{g60*D8Q9bwh#Z$D~x z+K7gc5*#G*k_cwg+712PT0@kq(JItb4KyP}D$eC~CIrVYU9Q?gX=O)>LLh$0ArWbd zO`Xw_^e98Eg`@J(Yu+&>xN5F_J%Sq4Z9}wR>Rj>%3f(&~sKg#<{v9!0lmucdCpDZ? z2_%Y^8v1sA^N(0i!|w?qklG`F{WYft;tlTX1ztub(PThf(Uj;AV|zz$+-0+6hY*lMFV%DN#Ozfh>5i zYYOL0wJ&ZELQ~1n%QOmPeBK)iAn+D}n@QmbYZ~5p{YI9RFglnF2yCB#1TpLFTFv>- z0KbJ4b($!h{G*T$SuO>D=jdU-r>n{U1*Zh&p8ptxV0M#N#mpTq2Zu#XYo$gor(qrg zXL|$8K$}jDau1rffFo{MW|1;&R&(|E%!K06#?$z>t#<9O5a){-4`gI}cl8l|$&^w6 zBMnm#4-g2y-g_*lSfB6r`O{)O ze*jNt0QQ$BROKwN|BC8_JYNz;36(r-v z%Ib8NE8=8G(ZLjV$)fbaaMy3_41zIt#U=PyLp0ApL0NU%7LOyDRW?I3FNE0yk(Ab9 zu6%0Ja%HASj3iNTLn5zV|0ieAxWB~~cIqYU9Z5YEy?TInK~_t(EVbNBAw9j6t8b6JZodscmbv;Qv;@Usb?ag4=Ffy76=L z1R}TQNbhEJu2;CgJXQ)xFAiX?nNF72oIY+9TU(*&nA;qPXxXVy5uf94s{k-yHgx0=h6g6^1(pB0&5o_>n+*NUaUT(}SrJ zM0s20=KIbgHX-w9V5(;DuV;c&B{8s*e9nA|3|Z8d@e=~c@;dB-&74rAP=Gg{%Y>|x8 z=YSU&4hDReoWMf~pf0+w&{SN=Hft4P2wp%j==>g$$`GvuiaW~>7`-k535m-Kn6jkQ zJ|GhTv*XFQM<_}11~l651(Z>c#v4M(C72$Nzx2}vdF=6bMXa-c_r@3)p%$aj5)#wwZ+S<2ag}IKuuzo;Q>3J2MTqaWh)8nApUwf)T{pCsyL2O zUg+_{a_8~jXir0+IGT3^DiB?qN7|)(H&1OYJhQ62fTULdFkU?BR?+M(7_w51mFi?q zwsexUN1;uThTCkxKqF#4CI#6GAk3b*fVuWk|G&g?0VOtuZ&3c9b!`g5>#iwnphdc zYVFU1_{kPAB5Z5m&FU3g4mt3jz~F7xBAY=S%>s!>o^Z6q$BS(|I&=l5+Q-9|O)Wq> zI7<$x&jNag%Iub`*&g_5Q+O)<2ViRO1OiLxO|SqXFfs>23kf8T4Qk#1agd`^D!Bc(;I%@V-2y+8eK#~tbO69-+reOb2G*d~V# z52}N5nNHDWgnKj5eh34^`omEdAe8XqhCC2xd7+QOY2Z9fLksx?j|T(y+-pkUIsR%` zf?F{qS`2x_9|9*lgKFRh=7X~j+jHps>&yYPkuSkvynV7a&Ez9-?1oLpp<;aE@DyDfhhhK(Fydv6bS!s%dWF5z>#h?ZnAS(o| zP7_256H9(URM2#GcxwICoVDg+RJIm51lcJL0`gy_IHL!28WW6X%|6%@rK zP`tFM?9g;wYiAAT$e53HfnS;W_JOc8HArR5jZDL@K8W4{5(V`6`lId|?pjO<8K8#$ e6Mr~z581mtU(LHmUbH6OPvwTj^@6Jw_x~4#0h|f| literal 14298 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