From 495a34108b3f196f2b81abddd895d9def42f4915 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Thu, 2 Nov 2023 16:46:12 +0100 Subject: [PATCH] Further implementation --- .../RealizationPicker/realizationPicker.tsx | 223 +++++++++++++--- .../components/RealizationPicker2/index.ts | 1 + .../RealizationPicker2/realizationPicker.tsx | 5 + .../InplaceVolumetricsNew/loadModule.tsx | 4 +- .../InplaceVolumetricsNew/settings.tsx | 23 +- .../modules/InplaceVolumetricsNew/state.ts | 12 +- .../modules/InplaceVolumetricsNew/view.tsx | 243 ++++++++++++++++++ 7 files changed, 469 insertions(+), 42 deletions(-) create mode 100644 frontend/src/framework/components/RealizationPicker2/index.ts create mode 100644 frontend/src/framework/components/RealizationPicker2/realizationPicker.tsx diff --git a/frontend/src/framework/components/RealizationPicker/realizationPicker.tsx b/frontend/src/framework/components/RealizationPicker/realizationPicker.tsx index aa42fed5c..4e4ec9036 100644 --- a/frontend/src/framework/components/RealizationPicker/realizationPicker.tsx +++ b/frontend/src/framework/components/RealizationPicker/realizationPicker.tsx @@ -13,11 +13,25 @@ type Selection = { value: string; }; +enum SelectionValidity { + Valid, + InputError, + Invalid, +} + +type SelectionValidityInfo = { + validity: SelectionValidity; + numMatchedRealizations: number; + numMatchedValidRealizations: number; +}; + type RealizationRangeTagProps = { uuid: string; active: boolean; caretPosition?: "start" | "end"; initialValue: string; + checkValidity: (value: string) => SelectionValidityInfo; + onChange: (value: string) => void; onRemove: () => void; onFocus: () => void; onKeyDown: (event: React.KeyboardEvent) => void; @@ -25,23 +39,10 @@ type RealizationRangeTagProps = { const realizationRangeRegex = /^\d+(\-\d+)?$/; -function checkIfValueIsValid(value: string): boolean { - if (!realizationRangeRegex.test(value)) { - return false; - } - - const range = value.split("-"); - if (range.length === 1) { - return parseInt(range[0]) >= 1; - } else if (range.length === 2) { - return parseInt(range[0]) >= 1 && parseInt(range[1]) >= parseInt(range[0]); - } - - return false; -} - const RealizationRangeTag: React.FC = (props) => { - const [valid, setValid] = React.useState(checkIfValueIsValid(props.initialValue)); + const [validityInfo, setValidityInfo] = React.useState( + props.checkValidity(props.initialValue) + ); const [value, setValue] = React.useState(props.initialValue); const [hasFocus, setHasFocus] = React.useState(false); @@ -62,11 +63,9 @@ const RealizationRangeTag: React.FC = (props) => { function handleChange(event: React.ChangeEvent) { const value = event.target.value; - if (checkIfValueIsValid(value)) { - setValid(true); - } else { - setValid(false); - } + setValidityInfo(props.checkValidity(value)); + props.onChange(value); + setValue(value); } function handleFocus(e: React.FocusEvent) { @@ -75,19 +74,56 @@ const RealizationRangeTag: React.FC = (props) => { setHasFocus(true); } + function makeTitle(): string | undefined { + if (validityInfo.validity === SelectionValidity.InputError) { + return "Invalid input"; + } else if (validityInfo.validity === SelectionValidity.Invalid) { + return "This value is not valid for the selected ensemble(s)"; + } + return undefined; + } + + function makeMatchCounter(): React.ReactNode { + if (validityInfo.numMatchedRealizations <= 1) { + return null; + } + + if (validityInfo.numMatchedValidRealizations === validityInfo.numMatchedRealizations) { + return ( + + {validityInfo.numMatchedRealizations} + + ); + } + + return ( + + {validityInfo.numMatchedValidRealizations}/{validityInfo.numMatchedRealizations} + + ); + } + return (
  • + {makeMatchCounter()} = (props) => { ); }; +function calcUniqueSelections(selections: Selection[], validRealizations?: Set): number[] { + const uniqueSelections = new Set(); + selections.forEach((selection) => { + const range = selection.value.split("-"); + if (range.length === 1) { + uniqueSelections.add(parseInt(range[0])); + } else if (range.length === 2) { + for (let i = parseInt(range[0]); i <= parseInt(range[1]); i++) { + uniqueSelections.add(i); + } + } + }); + + let uniqueSelectionsArray = Array.from(uniqueSelections); + + if (validRealizations) { + uniqueSelectionsArray = uniqueSelectionsArray.filter((realization) => validRealizations.has(realization)); + } + + return uniqueSelectionsArray.sort((a, b) => a - b); +} + export type RealizationPickerProps = { ensembleIdents: EnsembleIdent[]; + validRealizations?: Set; + debounceTimeMs?: number; onChange?: (selectedRealizations: number[]) => void; } & BaseComponentProps; @@ -123,32 +183,104 @@ export const RealizationPicker: React.FC = (props) => { const debounceTimeout = React.useRef | null>(null); const inputRef = React.useRef(null); - function calcUniqueSelections(): number[] { - const uniqueSelections = new Set(); - selections.forEach((selection) => { - const range = selection.value.split("-"); - if (range.length === 1) { - uniqueSelections.add(parseInt(range[0])); - } else if (range.length === 2) { + function checkValidity(value: string): SelectionValidityInfo { + if (!realizationRangeRegex.test(value)) { + return { + validity: SelectionValidity.InputError, + numMatchedRealizations: 0, + numMatchedValidRealizations: 0, + }; + } + + const range = value.split("-"); + if (range.length === 1) { + if (parseInt(range[0]) < 1) { + return { + validity: SelectionValidity.InputError, + numMatchedRealizations: 0, + numMatchedValidRealizations: 0, + }; + } + if (props.validRealizations) { + if (!props.validRealizations.has(parseInt(range[0]))) { + return { + validity: SelectionValidity.Invalid, + numMatchedRealizations: 1, + numMatchedValidRealizations: 0, + }; + } + } + return { + validity: SelectionValidity.Valid, + numMatchedRealizations: 1, + numMatchedValidRealizations: 1, + }; + } else if (range.length === 2) { + if (parseInt(range[0]) < 1 || parseInt(range[1]) <= parseInt(range[0])) { + return { + validity: SelectionValidity.InputError, + numMatchedRealizations: 0, + numMatchedValidRealizations: 0, + }; + } + const numMatches = parseInt(range[1]) - parseInt(range[0]) + 1; + if (props.validRealizations) { + let numNotValid = 0; for (let i = parseInt(range[0]); i <= parseInt(range[1]); i++) { - uniqueSelections.add(i); + if (!props.validRealizations.has(i)) { + numNotValid++; + } + } + if (numNotValid > 0) { + return { + validity: SelectionValidity.Invalid, + numMatchedRealizations: numMatches, + numMatchedValidRealizations: numMatches - numNotValid, + }; } } - }); - return Array.from(uniqueSelections).sort((a, b) => a - b); + return { + validity: SelectionValidity.Valid, + numMatchedRealizations: numMatches, + numMatchedValidRealizations: numMatches, + }; + } + + return { + validity: SelectionValidity.Valid, + numMatchedRealizations: 1, + numMatchedValidRealizations: 1, + }; + } + + function handleSelectionsChange(newSelections: Selection[]) { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + debounceTimeout.current = setTimeout(() => { + if (props.onChange) { + props.onChange(calcUniqueSelections(newSelections, props.validRealizations)); + } + }, props.debounceTimeMs || 0); } function handleChange(event: React.ChangeEvent) { const value = event.target.value; - setSelections((selections) => [...selections, { value, uuid: v4() }]); + const newSelections = [...selections, { value, uuid: v4() }]; + setSelections(newSelections); setActiveSelectionUuid(null); event.target.value = ""; + handleSelectionsChange(newSelections); } function handlePointerDown() { if (inputRef.current) { - inputRef.current.focus(); setActiveSelectionUuid(null); + setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.setSelectionRange(0, 0); + }, 500); } } @@ -172,7 +304,7 @@ export const RealizationPicker: React.FC = (props) => { } else if (event.key === "Enter" || event.key === ",") { event.preventDefault(); handleChange(event as any); - } else if (event.key === "Backspace") { + } else if (event.key === "Backspace" || event.key === "Delete" || event.key === "Home" || event.key === "End") { return; } else if (event.key === "ArrowLeft") { if (eventTarget.selectionStart === 0 && eventTarget.selectionEnd === 0) { @@ -209,12 +341,23 @@ export const RealizationPicker: React.FC = (props) => { } } + function handleTagValueChange(uuid: string, value: string) { + const newSelections = selections.map((selection) => { + if (selection.uuid === uuid) { + return { ...selection, value }; + } + return selection; + }); + setSelections(newSelections); + handleSelectionsChange(newSelections); + } + function clearSelections() { setSelections([]); setActiveSelectionUuid(null); } - const numSelectedRealizations = calcUniqueSelections().length; + const numSelectedRealizations = calcUniqueSelections(selections, props.validRealizations).length; return ( @@ -227,9 +370,11 @@ export const RealizationPicker: React.FC = (props) => { caretPosition={caretPosition} key={selection.uuid} initialValue={selection.value} + checkValidity={checkValidity} onRemove={() => handleRemove(selection.uuid)} onFocus={() => setActiveSelectionUuid(selection.uuid)} onKeyDown={handleKeyDown} + onChange={(value) => handleTagValueChange(selection.uuid, value)} /> ))}
  • diff --git a/frontend/src/framework/components/RealizationPicker2/index.ts b/frontend/src/framework/components/RealizationPicker2/index.ts new file mode 100644 index 000000000..ddc0c4aaf --- /dev/null +++ b/frontend/src/framework/components/RealizationPicker2/index.ts @@ -0,0 +1 @@ +export { RealizationPicker2 } from "./realizationPicker"; diff --git a/frontend/src/framework/components/RealizationPicker2/realizationPicker.tsx b/frontend/src/framework/components/RealizationPicker2/realizationPicker.tsx new file mode 100644 index 000000000..e71e01956 --- /dev/null +++ b/frontend/src/framework/components/RealizationPicker2/realizationPicker.tsx @@ -0,0 +1,5 @@ +export type RealizationPickerProps = {}; + +export const RealizationPicker2: React.FC = (props) => { + return null; +}; diff --git a/frontend/src/modules/InplaceVolumetricsNew/loadModule.tsx b/frontend/src/modules/InplaceVolumetricsNew/loadModule.tsx index 60eaf5409..1978583df 100644 --- a/frontend/src/modules/InplaceVolumetricsNew/loadModule.tsx +++ b/frontend/src/modules/InplaceVolumetricsNew/loadModule.tsx @@ -4,7 +4,9 @@ import { settings } from "./settings"; import { State } from "./state"; import { view } from "./view"; -const defaultState: State = {}; +const defaultState: State = { + subModules: [], +}; const module = ModuleRegistry.initModule("InplaceVolumetricsNew", defaultState); diff --git a/frontend/src/modules/InplaceVolumetricsNew/settings.tsx b/frontend/src/modules/InplaceVolumetricsNew/settings.tsx index e8b6a869a..fbb8bf8c3 100644 --- a/frontend/src/modules/InplaceVolumetricsNew/settings.tsx +++ b/frontend/src/modules/InplaceVolumetricsNew/settings.tsx @@ -1,6 +1,7 @@ import React from "react"; import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { EnsembleSet } from "@framework/EnsembleSet"; import { ModuleFCProps } from "@framework/Module"; import { useSettingsStatusWriter } from "@framework/StatusWriter"; import { useEnsembleSet, useIsEnsembleSetLoading } from "@framework/WorkbenchSession"; @@ -17,6 +18,20 @@ import { useTableNameAndMetadataFilterOptions } from "./hooks/useTableNameAndMet import { useTableNamesAndMetadata } from "./hooks/useTableNamesAndMetadata"; import { State } from "./state"; +function findValidRealizations(ensembleIdents: EnsembleIdent[], ensembleSet: EnsembleSet): Set { + const validRealizations: Set = new Set(); + for (const ensembleIdent of ensembleIdents) { + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + if (ensemble) { + for (const realization of ensemble.getRealizations()) { + validRealizations.add(realization); + } + } + } + + return validRealizations; +} + export const settings = ({ workbenchSession, moduleContext }: ModuleFCProps) => { const [selectedEnsembleIdents, setSelectedEnsembleIdents] = React.useState([]); const isEnsembleSetLoading = useIsEnsembleSetLoading(workbenchSession); @@ -36,6 +51,8 @@ export const settings = ({ workbenchSession, moduleContext }: ModuleFCProps; } + const validRealizations = findValidRealizations(selectedEnsembleIdents, ensembleSet); + return (
    } expanded> @@ -66,7 +83,11 @@ export const settings = ({ workbenchSession, moduleContext }: ModuleFCProps - +
    diff --git a/frontend/src/modules/InplaceVolumetricsNew/state.ts b/frontend/src/modules/InplaceVolumetricsNew/state.ts index 59e32fe7c..34fae6955 100644 --- a/frontend/src/modules/InplaceVolumetricsNew/state.ts +++ b/frontend/src/modules/InplaceVolumetricsNew/state.ts @@ -1 +1,11 @@ -export type State = {}; +export type SubModule = { + id: string; + relX: number; + relY: number; + relWidth: number; + relHeight: number; +}; + +export type State = { + subModules: SubModule[]; +}; diff --git a/frontend/src/modules/InplaceVolumetricsNew/view.tsx b/frontend/src/modules/InplaceVolumetricsNew/view.tsx index e7535bd16..35d131c06 100644 --- a/frontend/src/modules/InplaceVolumetricsNew/view.tsx +++ b/frontend/src/modules/InplaceVolumetricsNew/view.tsx @@ -1,11 +1,254 @@ import React from "react"; import { ModuleFCProps } from "@framework/Module"; +import { LayoutElement } from "@framework/Workbench"; +import { LayoutBox } from "@framework/components/LayoutBox"; +import { useElementSize } from "@lib/hooks/useElementSize"; +import { Point, Rect } from "@lib/utils/geometry"; import { State } from "./state"; export const view = (props: ModuleFCProps) => { const ref = React.useRef(null); + /* + + const [position, setPosition] = React.useState({ x: 0, y: 0 }); + const [pointer, setPointer] = React.useState({ x: -1, y: -1 }); + const [layout, setLayout] = React.useState([]); + const [tempLayoutBoxId, setTempLayoutBoxId] = React.useState(null); + const mainRef = React.useRef(null); + const size = useElementSize(ref); + const layoutBoxRef = React.useRef(null); + const subModules = props.moduleContext.useStoreValue("subModules"); + + const convertLayoutRectToRealRect = React.useCallback( + (element: LayoutElement): Rect => { + return { + x: element.relX * size.width, + y: element.relY * size.height, + width: element.relWidth * size.width, + height: element.relHeight * size.height, + }; + }, + [size] + ); + + React.useEffect(() => { + let pointerDownPoint: Point | null = null; + let pointerDownElementPosition: Point | null = null; + let pointerDownElementId: string | null = null; + let relativePointerPosition: Point = { x: 0, y: 0 }; + let pointerToElementDiff: Point = { x: 0, y: 0 }; + let dragging = false; + let moduleInstanceId: string | null = null; + let moduleName: string | null = null; + setLayout(props.workbench.getLayout()); + let originalLayout: LayoutElement[] = props.workbench.getLayout(); + let currentLayout: LayoutElement[] = props.workbench.getLayout(); + let originalLayoutBox = makeLayoutBoxes(originalLayout); + let currentLayoutBox = originalLayoutBox; + layoutBoxRef.current = currentLayoutBox; + let lastTimeStamp = 0; + let lastMovePosition: Point = { x: 0, y: 0 }; + let delayTimer: ReturnType | null = null; + let isNewModule = false; + + const adjustLayout = () => { + if (currentLayoutBox && moduleInstanceId) { + const preview = currentLayoutBox.previewLayout( + relativePointerPosition, + size, + moduleInstanceId, + isNewModule + ); + if (preview) { + currentLayout = preview.toLayout(); + currentLayoutBox = preview; + } + setLayout(currentLayout); + layoutBoxRef.current = currentLayoutBox; + } + delayTimer = null; + }; + + const handleModuleHeaderPointerDown = (payload: GuiEventPayloads[GuiEvent.ModuleHeaderPointerDown]) => { + console.debug("handleModuleHeaderPointerDown", payload); + pointerDownPoint = payload.pointerPosition; + pointerDownElementPosition = payload.elementPosition; + pointerDownElementId = payload.moduleInstanceId; + isNewModule = false; + }; + + const handleNewModulePointerDown = (payload: GuiEventPayloads[GuiEvent.NewModulePointerDown]) => { + pointerDownPoint = payload.pointerPosition; + pointerDownElementPosition = payload.elementPosition; + pointerDownElementId = v4(); + setTempLayoutBoxId(pointerDownElementId); + isNewModule = true; + moduleName = payload.moduleName; + }; + + const handlePointerUp = (e: PointerEvent) => { + if (!pointerDownPoint) { + return; + } + if (dragging) { + if (delayTimer) { + clearTimeout(delayTimer); + adjustLayout(); + } + if (isNewModule && moduleName) { + const layoutElement = currentLayout.find((el) => el.moduleInstanceId === pointerDownElementId); + if (layoutElement) { + const instance = props.workbench.makeAndAddModuleInstance(moduleName, layoutElement); + layoutElement.moduleInstanceId = instance.getId(); + layoutElement.moduleName = instance.getName(); + } + } + setDraggedModuleInstanceId(null); + if (isNewModule) { + setTempLayoutBoxId(null); + } + currentLayoutBox = makeLayoutBoxes(currentLayout); + originalLayoutBox = currentLayoutBox; + layoutBoxRef.current = currentLayoutBox; + setLayout(currentLayout); + props.workbench.setLayout(currentLayout); + setPosition({ x: 0, y: 0 }); + setPointer({ x: -1, y: -1 }); + e.stopPropagation(); + e.preventDefault(); + } + pointerDownPoint = null; + pointerDownElementPosition = null; + pointerDownElementId = null; + moduleInstanceId = null; + dragging = false; + document.body.classList.remove("select-none"); + originalLayout = currentLayout; + }; + + const handlePointerMove = (e: PointerEvent) => { + if (!pointerDownPoint || !ref.current || !pointerDownElementId || !pointerDownElementPosition) { + return; + } + if (!dragging) { + if (pointDistance(pointerEventToPoint(e), pointerDownPoint) > MANHATTAN_LENGTH) { + setDraggedModuleInstanceId(pointerDownElementId); + moduleInstanceId = pointerDownElementId; + const rect = ref.current.getBoundingClientRect(); + setPosition(pointRelativeToDomRect(pointerDownElementPosition, rect)); + relativePointerPosition = pointRelativeToDomRect(pointerDownPoint, rect); + document.body.classList.add("select-none"); + dragging = true; + pointerToElementDiff = pointDifference(pointerDownPoint, pointerDownElementPosition); + lastTimeStamp = e.timeStamp; + lastMovePosition = pointerEventToPoint(e); + } + } else { + if (!pointerDownElementId || !pointerDownPoint) { + return; + } + const rect = ref.current.getBoundingClientRect(); + setPosition(pointDifference(pointDifference(pointerEventToPoint(e), rect), pointerToElementDiff)); + setPointer(pointDifference(pointerEventToPoint(e), rect)); + relativePointerPosition = pointDifference(pointerEventToPoint(e), rect); + const speed = pointDistance(pointerEventToPoint(e), lastMovePosition) / (e.timeStamp - lastTimeStamp); + lastTimeStamp = e.timeStamp; + lastMovePosition = pointerEventToPoint(e); + + if (!rectContainsPoint(addMarginToRect(rect, 25), pointerEventToPoint(e))) { + currentLayout = originalLayout; + currentLayoutBox = originalLayoutBox; + setLayout(currentLayout); + layoutBoxRef.current = currentLayoutBox; + if (delayTimer) { + clearTimeout(delayTimer); + delayTimer = null; + } + return; + } + + if (delayTimer && speed > 0.5) { + clearTimeout(delayTimer); + delayTimer = null; + } + if (delayTimer === null) { + delayTimer = setTimeout(adjustLayout, 500); + } + } + }; + + const handleButtonClick = (e: KeyboardEvent) => { + if (e.key === "Escape") { + if (delayTimer) { + clearTimeout(delayTimer); + } + setLayout(originalLayout); + currentLayout = originalLayout; + pointerDownPoint = null; + pointerDownElementPosition = null; + pointerDownElementId = null; + setDraggedModuleInstanceId(null); + moduleInstanceId = null; + dragging = false; + document.body.classList.remove("select-none"); + originalLayout = currentLayout; + currentLayoutBox = makeLayoutBoxes(currentLayout); + originalLayoutBox = currentLayoutBox; + setLayout(currentLayout); + isNewModule = false; + setTempLayoutBoxId(null); + } + }; + + const handleRemoveModuleInstanceRequest = (payload: GuiEventPayloads[GuiEvent.RemoveModuleInstanceRequest]) => { + if (delayTimer) { + clearTimeout(delayTimer); + } + if (dragging) { + return; + } + props.workbench.removeModuleInstance(payload.moduleInstanceId); + currentLayoutBox.removeLayoutElement(payload.moduleInstanceId); + currentLayout = currentLayoutBox.toLayout(); + setLayout(currentLayout); + originalLayout = currentLayout; + originalLayoutBox = currentLayoutBox; + props.workbench.setLayout(currentLayout); + }; + + const removeModuleHeaderPointerDownSubscriber = guiMessageBroker.subscribeToEvent( + GuiEvent.ModuleHeaderPointerDown, + handleModuleHeaderPointerDown + ); + const removeNewModulePointerDownSubscriber = guiMessageBroker.subscribeToEvent( + GuiEvent.NewModulePointerDown, + handleNewModulePointerDown + ); + const removeRemoveModuleInstanceRequestSubscriber = guiMessageBroker.subscribeToEvent( + GuiEvent.RemoveModuleInstanceRequest, + handleRemoveModuleInstanceRequest + ); + + document.addEventListener("pointerup", handlePointerUp); + document.addEventListener("pointermove", handlePointerMove); + document.addEventListener("keydown", handleButtonClick); + + return () => { + removeModuleHeaderPointerDownSubscriber(); + removeNewModulePointerDownSubscriber(); + removeRemoveModuleInstanceRequestSubscriber(); + + document.removeEventListener("pointerup", handlePointerUp); + document.removeEventListener("pointermove", handlePointerMove); + document.removeEventListener("keydown", handleButtonClick); + if (delayTimer) { + clearTimeout(delayTimer); + } + }; + }, [size, moduleInstances]); + */ return (