diff --git a/src/controllers.ts b/src/controllers.ts index 2d0d178..468ad6e 100644 --- a/src/controllers.ts +++ b/src/controllers.ts @@ -134,12 +134,27 @@ class PointerController { } }; + // fuzzy detection of mouse wheel events vs trackpad events + const isMouseEvent = (deltaX: number, deltaY: number) => { + return (Math.abs(deltaX) > 50 && deltaY === 0) || + (Math.abs(deltaY) > 50 && deltaX === 0) || + (deltaX === 0 && deltaY !== 0) && !Number.isInteger(deltaY); + }; + const wheel = (event: WheelEvent) => { - const sign = (v: number) => v > 0 ? 1 : v < 0 ? -1 : 0; - zoom(sign(event.deltaY) * -0.2); - if (Math.abs(event.deltaX) > 1e-6) { - orbit(sign(event.deltaX) * 2.0, 0); + const { deltaX, deltaY } = event; + + if (isMouseEvent(deltaX, deltaY)) { + zoom(deltaY * -0.002); + } else if (event.ctrlKey || event.metaKey) { + zoom(deltaY * -0.02); + } else if (event.shiftKey) { + pan(event.offsetX, event.offsetY, deltaX, deltaY); + } else { + orbit(deltaX, deltaY); } + + event.preventDefault(); }; // FIXME: safari sends canvas as target of dblclick event but chrome sends the target element @@ -187,12 +202,12 @@ class PointerController { let destroy: () => void = null; - const wrap = (target: any, name: string, fn: any) => { + const wrap = (target: any, name: string, fn: any, options?: any) => { const callback = (event: any) => { camera.scene.events.fire('camera.controller', name); fn(event); }; - target.addEventListener(name, callback); + target.addEventListener(name, callback, options); destroy = () => { destroy?.(); target.removeEventListener(name, callback); @@ -202,7 +217,7 @@ class PointerController { wrap(target, 'pointerdown', pointerdown); wrap(target, 'pointerup', pointerup); wrap(target, 'pointermove', pointermove); - wrap(target, 'wheel', wheel); + wrap(target, 'wheel', wheel, { passive: false }); wrap(target, 'dblclick', dblclick); wrap(document, 'keydown', keydown); wrap(document, 'keyup', keyup); diff --git a/src/drop-handler.ts b/src/drop-handler.ts index da7734d..09ae362 100644 --- a/src/drop-handler.ts +++ b/src/drop-handler.ts @@ -95,10 +95,12 @@ const CreateDropHandler = (target: HTMLElement, dropHandler: DropHandlerFunc) => const drop = async (ev: DragEvent) => { ev.preventDefault(); + const items = Array.from(ev.dataTransfer.items) + .map(item => item.webkitGetAsEntry()) + .filter(v => v); + // resolve directories to files - const entries = await resolveDirectories( - Array.from(ev.dataTransfer.items).map(item => item.webkitGetAsEntry()) - ); + const entries = await resolveDirectories(items); const files = await Promise.all( entries.map(entry => { diff --git a/src/file-handler.ts b/src/file-handler.ts index 95a5b8f..f7f66a0 100644 --- a/src/file-handler.ts +++ b/src/file-handler.ts @@ -149,11 +149,11 @@ const initFileHandler = async (scene: Scene, events: Events, dropTarget: HTMLEle } else { throw new Error(`Unsupported file type`); } - } catch (err) { + } catch (error) { events.invoke('showPopup', { type: 'error', header: localize('popup.error-loading'), - message: `${err.message ?? err} while loading '${filename}'` + message: `${error.message ?? error} while loading '${filename}'` }); } }; @@ -185,9 +185,17 @@ const initFileHandler = async (scene: Scene, events: Events, dropTarget: HTMLEle // create the file drag & drop handler CreateDropHandler(dropTarget, async (entries) => { - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - await handleLoad(entry.url, entry.filename); + if (entries.length === 0) { + events.invoke('showPopup', { + type: 'error', + header: localize('popup.error-loading'), + message: localize('popup.drop-files') + }); + } else { + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + await handleLoad(entry.url, entry.filename); + } } }); @@ -413,11 +421,11 @@ const initFileHandler = async (scene: Scene, events: Events, dropTarget: HTMLEle await writeScene(options.type, writeFunc); download(options.filename, (cursor === data.byteLength) ? data : new Uint8Array(data.buffer, 0, cursor)); } - } catch (err) { + } catch (error) { events.invoke('showPopup', { type: 'error', header: localize('popup.error-loading'), - message: `${err.message ?? err} while saving file` + message: `${error.message ?? error} while saving file` }); } finally { events.fire('stopSpinner'); diff --git a/src/index.html b/src/index.html index 2b62aff..a254c6d 100644 --- a/src/index.html +++ b/src/index.html @@ -27,9 +27,12 @@ diff --git a/src/tools/brush-selection.ts b/src/tools/brush-selection.ts index 2136e5c..02b04e8 100644 --- a/src/tools/brush-selection.ts +++ b/src/tools/brush-selection.ts @@ -105,9 +105,9 @@ class BrushSelection { }; const wheel = (e: WheelEvent) => { - const delta = e.deltaX + e.deltaY; - if (e.shiftKey && delta !== 0) { - events.fire(delta > 0 ? 'tool.brushSelection.smaller' : 'tool.brushSelection.bigger'); + if (e.altKey || e.metaKey) { + const { deltaX, deltaY } = e; + events.fire((Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY) > 0 ? 'tool.brushSelection.smaller' : 'tool.brushSelection.bigger'); e.preventDefault(); e.stopPropagation(); } diff --git a/src/ui/editor.ts b/src/ui/editor.ts index c8f4aa4..9681dcd 100644 --- a/src/ui/editor.ts +++ b/src/ui/editor.ts @@ -179,10 +179,11 @@ class EditorUI { canvas.width = Math.ceil(canvasContainer.dom.offsetWidth * pixelRatio); canvas.height = Math.ceil(canvasContainer.dom.offsetHeight * pixelRatio); - // disable context menu globally - document.addEventListener('contextmenu', (event: MouseEvent) => { - event.preventDefault(); - }, true); + ['contextmenu', 'gesturestart', 'gesturechange', 'gestureend'].forEach((event) => { + document.addEventListener(event, (e) => { + e.preventDefault(); + }, true); + }); // whenever the canvas container is clicked, set keyboard focus on the body canvasContainer.dom.addEventListener('pointerdown', (event: PointerEvent) => { diff --git a/src/ui/localization.ts b/src/ui/localization.ts index ac04060..3ea7d40 100644 --- a/src/ui/localization.ts +++ b/src/ui/localization.ts @@ -141,6 +141,7 @@ const localizeInit = () => { "popup.yes": "Ja", "popup.no": "Nein", "popup.error-loading": "FEHLER BEIM LADEN DER DATEI", + "popup.drop-files": "Bitte Dateien und Ordner ablegen", // Right toolbar "tooltip.splat-mode": "Splat Modus ( M )", @@ -301,6 +302,7 @@ const localizeInit = () => { "popup.yes": "Yes", "popup.no": "No", "popup.error-loading": "ERROR LOADING FILE", + "popup.drop-files": "Please drop files and folders", // Right toolbar "tooltip.splat-mode": "Splat Mode ( M )", @@ -452,6 +454,7 @@ const localizeInit = () => { "popup.yes": "Oui", "popup.no": "Non", "popup.error-loading": "Erreur de chargement du fichier", + "popup.drop-files": "Veuillez déposer des fichiers et des dossiers", // right toolbar "tooltip.splat-mode": "Mode splat ( M )", @@ -603,6 +606,7 @@ const localizeInit = () => { "popup.yes": "はい", "popup.no": "いいえ", "popup.error-loading": "ファイルの読み込みエラー", + "popup.drop-files": "ファイルやフォルダをドロップしてください", // Right toolbar "tooltip.splat-mode": "スプラットモード ( M )", @@ -754,6 +758,7 @@ const localizeInit = () => { "popup.yes": "예", "popup.no": "아니요", "popup.error-loading": "파일 로드 오류", + "popup.drop-files": "파일 및 폴더를 드롭하세요", // Right toolbar "tooltip.splat-mode": "Splat 모드 ( M )", @@ -905,6 +910,7 @@ const localizeInit = () => { "popup.yes": "是", "popup.no": "否", "popup.error-loading": "加载文件错误", + "popup.drop-files": "请拖放文件和文件夹", // Right toolbar "tooltip.splat-mode": "Splat 模式 ( M )", diff --git a/src/ui/scss/style.scss b/src/ui/scss/style.scss index 1cbff30..ff1b7b5 100644 --- a/src/ui/scss/style.scss +++ b/src/ui/scss/style.scss @@ -21,6 +21,7 @@ * { font-size: 12px; user-select: none; + overscroll-behavior: none; } html {