diff --git a/src/Renderer/Renderer.fsproj b/src/Renderer/Renderer.fsproj index 4d6dc50dc..729ac3748 100644 --- a/src/Renderer/Renderer.fsproj +++ b/src/Renderer/Renderer.fsproj @@ -103,9 +103,13 @@ - + + + + + diff --git a/src/Renderer/UI/MainView.fs b/src/Renderer/UI/MainView.fs index 23884d18b..b759186a7 100644 --- a/src/Renderer/UI/MainView.fs +++ b/src/Renderer/UI/MainView.fs @@ -335,7 +335,7 @@ let displayView model dispatch = newWidth |> max minViewerWidth |> min (windowX - minEditorWidth()) - setViewerWidthInWaveSim w dispatch + WaveSimNavigation.setViewerWidthInWaveSim w dispatch dispatch <| SetDragMode DragModeOff dispatch <| SetViewerWidth w | _ -> () diff --git a/src/Renderer/UI/Update.fs b/src/Renderer/UI/Update.fs index 3017ad8a4..22439e856 100644 --- a/src/Renderer/UI/Update.fs +++ b/src/Renderer/UI/Update.fs @@ -17,6 +17,7 @@ open Optics open Optics.Optic open Optics.Operators + //---------------------------------------------------------------------------------------------// //---------------------------------------------------------------------------------------------// //---------------------------------- Update Model ---------------------------------------------// @@ -67,7 +68,7 @@ let update (msg : Msg) oldModel = match testMsg with | ChangeWaveSimMultiplier key -> - let table = WaveSimHelpers.Constants.multipliers + let table = WaveSimStyle.Constants.multipliers if key < 0 || key >= table.Length then printf $"Warning: Can't chnage multiplier to key = {key}" model, Cmd.none @@ -80,7 +81,7 @@ let update (msg : Msg) oldModel = model |> Optic.map waveSim_ (fun ws -> let wsModel = ws[sheet] - Map.add sheet (WaveSimHelpers.changeMultiplier (table[key]) wsModel) ws) + Map.add sheet (WaveSimNavigation.changeMultiplier (table[key]) wsModel) ws) |> (fun m -> m, Cmd.none) | CheckMemory -> if JSHelpers.loggingMemory then @@ -256,18 +257,18 @@ let update (msg : Msg) oldModel = | SetWaveComponentSelectionOpen (fIdL, show) -> model - |> updateWSModel (fun ws -> WaveSimHelpers.setWaveComponentSelectionOpen ws fIdL show) + |> updateWSModel (fun ws -> WaveSimStyle.setWaveComponentSelectionOpen ws fIdL show) |> withNoMsg | SetWaveGroupSelectionOpen (fIdL, show) -> model - |> updateWSModel (fun ws -> WaveSimHelpers.setWaveGroupSelectionOpen ws fIdL show) + |> updateWSModel (fun ws -> WaveSimStyle.setWaveGroupSelectionOpen ws fIdL show) |> withNoMsg | SetWaveSheetSelectionOpen (fIdL, show) -> model - |> updateWSModel (fun ws -> WaveSimHelpers.setWaveSheetSelectionOpen ws fIdL show) + |> updateWSModel (fun ws -> WaveSimStyle.setWaveSheetSelectionOpen ws fIdL show) |> withNoMsg | TryStartSimulationAfterErrorFix simType -> @@ -609,7 +610,7 @@ let update (msg : Msg) oldModel = | ScrollbarMouseMsg (cursor: float, action: ScrollbarMouseAction, dispatch: Msg->unit) -> let wsm = Map.find (Option.get model.WaveSimSheet) model.WaveSim - WaveSim.updateScrollbar wsm dispatch cursor action + WaveSimNavigation.updateScrollbar wsm dispatch cursor action model, Cmd.none // Various messages here that are not implemented as yet, or are no longer used diff --git a/src/Renderer/UI/UpdateHelpers.fs b/src/Renderer/UI/UpdateHelpers.fs index d182ef573..4464b8a61 100644 --- a/src/Renderer/UI/UpdateHelpers.fs +++ b/src/Renderer/UI/UpdateHelpers.fs @@ -65,7 +65,7 @@ let shortDWSM (ws: WaveSimModel) = let shortDisplayMsg (msg:Msg) = match msg with | ChangeWaveSimMultiplier n -> - List.tryItem n WaveSimHelpers.Constants.multipliers + List.tryItem n WaveSimStyle.Constants.multipliers |> Option.map (fun n -> $"Set WS multiplier to {n}") |> Option.defaultValue $"Invalid Ws mult key of {n}" |> Some diff --git a/src/Renderer/UI/WaveSim/WaveSim.fs b/src/Renderer/UI/WaveSim/WaveSim.fs index 87b3efa8e..f7c7fe189 100644 --- a/src/Renderer/UI/WaveSim/WaveSim.fs +++ b/src/Renderer/UI/WaveSim/WaveSim.fs @@ -13,1078 +13,11 @@ open TopMenuView open SimulatorTypes open NumberHelpers open DrawModelType -open WaveSimSelect +open WaveSimNavigation +//open WaveSimSelect open DiagramStyle - -module Constants = - /// Config variable to choose whether to generate the full 1000 cycles of SVG. - let generateVisibleOnly = true - /// Config variable to choose whether to print performance analysis info to console. - let showPerfLogs = false - let inlineNoWrap = WhiteSpace WhiteSpaceOptions.Nowrap - -open Constants - - -/// Generates SVG to display non-binary values on waveforms. -/// Should be refactored together with displayBigIntOnWave. -let displayUInt32OnWave - (wsModel: WaveSimModel) - (width: int) - (waveValues: array) - (transitions: array) - : list = - // find all clock cycles where there is a NonBinaryTransition.Change - let changeTransitions = - transitions - |> Array.indexed - |> Array.filter (fun (_, x) -> x = Change) - |> Array.map (fun (i, _) -> i) - - // find start and length of each gap between a Change transition - let gaps: array = - if Constants.generateVisibleOnly - then - // add dummy change at visible end, but need account for difference in changes: - // e.g. if we are showing 3 cycles, a wave with a change in each would be 0, 1, 2, 3 and would be fine when - // 4 is added; however, a wave with no change at all would be 0, and would produce an errorneous gap length - // of 4 when 4 is added - we therefore add 3 - if changeTransitions[Array.length changeTransitions-1] <> wsModel.ShownCycles - then Array.append changeTransitions [|wsModel.ShownCycles|] - else Array.append changeTransitions [|wsModel.ShownCycles+1|] - |> Array.map (fun loc -> loc+wsModel.StartCycle) // shift cycle to start cycle - else - Array.append changeTransitions [|wsModel.StartCycle+transitions.Length-1|] // add dummry change length end - |> Array.pairwise - |> Array.map (fun (i1, i2) -> {Start = i1; Length = i2-i1}) // get start and length of gap - - // utility functions for SVG generation - /// Function to make polygon fill for a gap. - /// Array of polyline points to fill. - let makePolyfill (points: array) = - let points = points |> Array.distinct - polyline (wavePolyfillStyle points) [] - - /// Function to make text element for a gap. - /// Starting X location of element. - let makeTextElement (start: float) (waveValue: string) = - text (singleValueOnWaveProps start) [ str waveValue ] - - // create text element for every gap - gaps - |> Array.map (fun gap -> - // generate string - let waveValue = UInt32ToPaddedString Constants.waveLegendMaxChars wsModel.Radix width waveValues[gap.Start] - - // calculate display widths - let cycleWidth = singleWaveWidth wsModel - let gapWidth = (float gap.Length * cycleWidth) - 2. * Constants.nonBinaryTransLen - let singleWidth = 1.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue - let doubleWidth = 2. * singleWidth + Constants.valueOnWavePadding - - match gapWidth with - | w when (w < singleWidth) -> // display filled polygon - let fillPoints = nonBinaryFillPoints cycleWidth gap - let fill = makePolyfill fillPoints - [ fill ] - | w when (singleWidth <= w && w < doubleWidth) -> // diplay 1 copy at centre - let gapCenterPadWidth = (float gap.Length * cycleWidth - singleWidth) / 2. - let singleText = makeTextElement (float gap.Start * cycleWidth + gapCenterPadWidth) waveValue - [ singleText ] - | w when (doubleWidth <= w) -> // display 2 copies at end of gaps - let singleCycleCenterPadWidth = // if a single cycle gap can include 2 copies, set arbitrary padding - if cycleWidth < doubleWidth - then (cycleWidth - singleWidth) / 2. - else Constants.valueOnWaveEdgePadding - let startPadWidth = - if singleCycleCenterPadWidth < 0.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue - then 0.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue - else singleCycleCenterPadWidth - let endPadWidth = (float gap.Length * cycleWidth - startPadWidth - singleWidth) - let startText = makeTextElement (float gap.Start * cycleWidth + startPadWidth) waveValue - let endText = makeTextElement (float gap.Start * cycleWidth + endPadWidth) waveValue - [ startText; endText ] - | _ -> // catch-all - failwithf "displayUInt32OnWave: impossible case" - ) - |> List.concat - -/// Generates SVG to display bigint values on waveforms. -/// Should be refactored together with displayUInt32OnWave. -let displayBigIntOnWave - (wsModel: WaveSimModel) - (width: int) - (waveValues: array) - (transitions: array) - : list = - // find all clock cycles where there is a NonBinaryTransition.Change - let changeTransitions = - transitions - |> Array.indexed - |> Array.filter (fun (_, x) -> x = Change) - |> Array.map (fun (i, _) -> i) - - // find start and length of each gap between a Change transition - let gaps: array = - if Constants.generateVisibleOnly - then - // add dummy change at visible end, but need account for difference in changes: - // e.g. if we are showing 3 cycles, a wave with a change in each would be 0, 1, 2, 3 and would be fine when - // 4 is added; however, a wave with no change at all would be 0, and would produce an errorneous gap length - // of 4 when 4 is added - we therefore add 3 - if changeTransitions[Array.length changeTransitions-1] <> wsModel.ShownCycles - then Array.append changeTransitions [|wsModel.ShownCycles|] - else Array.append changeTransitions [|wsModel.ShownCycles+1|] - |> Array.map (fun loc -> loc+wsModel.StartCycle) // shift cycle to start cycle - else - Array.append changeTransitions [|wsModel.StartCycle+transitions.Length-1|] // add dummry change length end - |> Array.pairwise - |> Array.map (fun (i1, i2) -> {Start = i1; Length = i2-i1}) // get start and length of gap - - // utility functions for SVG generation - /// Function to make polygon fill for a gap. - /// Array of polyline points to fill. - let makePolyfill (points: array) = - let points = points |> Array.distinct - polyline (wavePolyfillStyle points) [] - - /// Function to make text element for a gap. - /// Starting X location of element. - let makeTextElement (start: float) (waveValue: string) = - text (singleValueOnWaveProps start) [ str waveValue ] - - // create text element for every gap - gaps - |> Array.map (fun gap -> - // generate string - let waveValue = BigIntToPaddedString Constants.waveLegendMaxChars wsModel.Radix width waveValues[gap.Start] - - // calculate display widths - let cycleWidth = singleWaveWidth wsModel - let gapWidth = (float gap.Length * cycleWidth) - 2. * Constants.nonBinaryTransLen - let singleWidth = 1.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue - let doubleWidth = 2. * singleWidth + Constants.valueOnWavePadding - - match gapWidth with - | w when (w < singleWidth) -> // display filled polygon - let fillPoints = nonBinaryFillPoints cycleWidth gap - let fill = makePolyfill fillPoints - [ fill ] - | w when (singleWidth <= w && w < doubleWidth) -> // diplay 1 copy at centre - let gapCenterPadWidth = (float gap.Length * cycleWidth - singleWidth) / 2. - let singleText = makeTextElement (float gap.Start * cycleWidth + gapCenterPadWidth) waveValue - [ singleText ] - | w when (doubleWidth <= w) -> // display 2 copies at end of gaps - let singleCycleCenterPadWidth = // if a single cycle gap can include 2 copies, set arbitrary padding - if cycleWidth < doubleWidth - then (cycleWidth - singleWidth) / 2. - else Constants.valueOnWaveEdgePadding - let startPadWidth = - if singleCycleCenterPadWidth < 0.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue - then 0.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue - else singleCycleCenterPadWidth - let endPadWidth = (float gap.Length * cycleWidth - startPadWidth - singleWidth) - let startText = makeTextElement (float gap.Start * cycleWidth + startPadWidth) waveValue - let endText = makeTextElement (float gap.Start * cycleWidth + endPadWidth) waveValue - [ startText; endText ] - | _ -> // catch-all - failwithf "displayUInt32OnWave: impossible case" - ) - |> List.concat - -/// Check if generated SVG is correct, based on existence, position, -/// and zoom. Fast simulation data is assumed unchanged. Used to determine if -/// generateWaveform is run. -let waveformIsUptodate (ws: WaveSimModel) (wave: Wave): bool = - wave.SVG <> None && - wave.ShownCycles = ws.ShownCycles && - wave.StartCycle = ws.StartCycle && - wave.CycleWidth = singleWaveWidth ws && - wave.Radix = ws.Radix && - wave.Multiplier = ws.CycleMultiplier - -/// Called when InitiateWaveSimulation message is dispatched and when wave -/// simulator is refreshed. Generates or updates the SVG for a specific waveform -/// whether needed or not. The SVG depends on cycle width as well as start/stop -/// clocks and design. Assumes that the fast simulation data has not changed and -/// has enough cycles. -let generateWaveform (ws: WaveSimModel) (index: WaveIndexT) (wave: Wave): Wave = - let makePolyline points = - let points = points |> Array.concat |> Array.distinct - polyline (wavePolylineStyle points) [] - - let waveform = - match wave.Width with - | 0 -> - failwithf "Cannot have wave of width 0" - - | 1 -> // binary waveform - let transitions = calculateBinaryTransitionsUInt32 wave.WaveValues.UInt32Step ws.StartCycle ws.ShownCycles ws.CycleMultiplier - - let wavePoints = - let waveWidth = singleWaveWidth ws - let startCycle = if Constants.generateVisibleOnly then ws.StartCycle else 0 - Array.mapi (binaryWavePoints waveWidth startCycle) transitions - |> Array.concat - |> Array.distinct - - svg (waveRowProps ws) [ polyline (wavePolylineStyle wavePoints) [] ] - - | w when w <= 32 -> // non-binary waveform - let transitions = calculateNonBinaryTransitions wave.WaveValues.UInt32Step ws.StartCycle ws.ShownCycles ws.CycleMultiplier - let fstPoints, sndPoints = - let waveWidth = singleWaveWidth ws - let startCycle = if Constants.generateVisibleOnly then ws.StartCycle else 0 - Array.mapi (nonBinaryWavePoints waveWidth startCycle) transitions |> Array.unzip - - let valuesSVG = displayUInt32OnWave ws wave.Width wave.WaveValues.UInt32Step transitions - let polyLines = [makePolyline fstPoints; makePolyline sndPoints] - - svg (waveRowProps ws) (List.append polyLines valuesSVG) - - | _ -> // non-binary waveform with width greather than 32 - let transitions = calculateNonBinaryTransitions wave.WaveValues.UInt32Step ws.StartCycle ws.ShownCycles ws.CycleMultiplier - - let fstPoints, sndPoints = - Array.mapi (nonBinaryWavePoints (singleWaveWidth ws) 0) transitions |> Array.unzip - - let valuesSVG = displayBigIntOnWave ws wave.Width wave.WaveValues.BigIntStep transitions - - svg (waveRowProps ws) (List.append [makePolyline fstPoints; makePolyline sndPoints] valuesSVG) - {wave with - Radix = ws.Radix - ShownCycles = ws.ShownCycles - StartCycle = ws.StartCycle - Multiplier = ws.CycleMultiplier - CycleWidth = singleWaveWidth ws - SVG = Some waveform} - - - -/// Set highlighted clock cycle number -let private setClkCycle (wsModel: WaveSimModel) (dispatch: Msg -> unit) (newRealClkCycle: int) : unit = - let start = TimeHelpers.getTimeMs () - let newDetail = max newRealClkCycle 0 - let mult = wsModel.CycleMultiplier - let newClkCycle = newRealClkCycle / mult - let newClkCycle = min Constants.maxLastClk newClkCycle |> max 0 - - if newClkCycle <= endCycle wsModel then - if newClkCycle < wsModel.StartCycle then - dispatch <| GenerateWaveforms - {wsModel with - StartCycle = newClkCycle - CurrClkCycle = newClkCycle - ClkCycleBoxIsEmpty = false - CurrClkCycleDetail = newDetail - } - else - dispatch <| SetWSModel - {wsModel with - CurrClkCycle = newClkCycle - ClkCycleBoxIsEmpty = false - CurrClkCycleDetail = newDetail - } - else - let newDetail = min newDetail maxLastClk - let newClkCycle = newDetail / mult - dispatch <| GenerateWaveforms - {wsModel with - StartCycle = newClkCycle - (wsModel.ShownCycles - 1) - CurrClkCycle = newClkCycle - ClkCycleBoxIsEmpty = false - CurrClkCycleDetail = newDetail - } - |> TimeHelpers.instrumentInterval "setClkCycle" start - -/// Move waveform view window by closest integer number of cycles. -/// Current clock cycle (WaveSimModel.CurrClkCycle) is set to beginning or end depending on direction of movement. -/// Update is achieved by dispatching a GenerateWaveforms message. -/// Note the side-effect of clearing the ScrollbarQueueIsEmpty counter. -/// Target WaveSimModel. -/// Dispatch function to send messages with. -/// Number of non-integer cycles to move by. -let setScrollbarTbByCycs (wsm: WaveSimModel) (dispatch: Msg->unit) (moveByCycs: float): unit = - let moveWindowBy = int (System.Math.Round moveByCycs) - let mult = wsm.CycleMultiplier - - /// Return target value when within min and max value, otherwise min or max. - let bound (minV: int) (maxV: int) (tarV: int): int = tarV |> max minV |> min maxV - let minSimCyc = 0 - let maxSimCyc = Constants.maxLastClk / mult - - let newStartCyc = (wsm.StartCycle+moveWindowBy) |> bound minSimCyc (maxSimCyc-wsm.ShownCycles+1) - let newCurrCyc = - let newEndCyc = newStartCyc+wsm.ShownCycles-1 - if newStartCyc <= wsm.CurrClkCycle && wsm.CurrClkCycle <= newEndCyc - then - wsm.CurrClkCycle - else - if abs (wsm.CurrClkCycle - newStartCyc) < abs (wsm.CurrClkCycle - newEndCyc) - then newStartCyc - else newEndCyc - let detail = wsm.CurrClkCycleDetail - let detail = if newCurrCyc <> detail / mult then newCurrCyc * mult else detail - GenerateWaveforms { - wsm with StartCycle = newStartCyc; - CurrClkCycle = newCurrCyc; - CurrClkCycleDetail = detail; - ScrollbarQueueIsEmpty = true } |> dispatch - -/// Update WaveSimModel with new ScrollbarTbOffset. -/// Used when starting or clearing scrollbar drag mode. -/// Update is achieved by dispatching a GenerateWaveforms message. -/// Target WaveSimModel. -/// Dispatch function to send messages with. -/// Offset option to be written to WaveSimModel.ScrollbarTbOffset. -let setScrollbarOffset (wsm: WaveSimModel) (dispatch: Msg->unit) (offset: float option): unit = - GenerateWaveforms { wsm with ScrollbarTbOffset = offset; ScrollbarQueueIsEmpty = true } |> dispatch - -/// Update WaveSimModel with new ScrollbarQueueIsEmpty. -/// Used to update is-empty counter to coalesce scrollbar mouse events together. -/// Update is achieved by dispatching a UpdateWSModel message, so as to not clog the queue with GenerateWaveforms messages. -/// Target WaveSimModel. -/// Dispatch function to send messages with. -/// Bool to be written to WaveSimModel.ScrollbarQueueIsEmpty. -let setScrollbarLastX (wsm: WaveSimModel) (dispatch: Msg->unit) (isEmpty: bool): unit = - UpdateWSModel (fun _ -> { wsm with ScrollbarQueueIsEmpty = isEmpty }) |> dispatch - -/// If zoomIn, then increase width of clock cycles (i.e.reduce number of visible cycles). -/// otherwise reduce width. GenerateWaveforms message will reconstitute SVGs after the change. -let changeZoom (wsModel: WaveSimModel) (zoomIn: bool) (dispatch: Msg -> unit) = - let start = TimeHelpers.getTimeMs () - let shownCycles = - let wantedCycles = int (float wsModel.ShownCycles / Constants.zoomChangeFactor) - if zoomIn then - // try to reduce number of cycles displayed - wantedCycles - // If number of cycles after casting to int does not change - |> (fun nc -> if nc = wsModel.ShownCycles then nc - 1 else nc ) - // Require a minimum of cycles - |> (fun nc -> - let minVis = min wsModel.ShownCycles Constants.minVisibleCycles - max nc minVis) - else - let wantedCycles = int (float wsModel.ShownCycles * Constants.zoomChangeFactor) - // try to increase number of cycles displayed - wantedCycles - // If number of cycles after casting to int does not change - |> (fun nc -> if nc = wsModel.ShownCycles then nc + 1 else nc ) - |> (fun nc -> - let maxNc = int (wsModel.WaveformColumnWidth / float Constants.minCycleWidth) - max wsModel.ShownCycles (min nc maxNc)) - let startCycle = - // preferred start cycle to keep centre of screen ok - let sc = (wsModel.StartCycle - (shownCycles - wsModel.ShownCycles)/2) - let cOffset = wsModel.CurrClkCycle - sc - sc - // try to keep cursor on screen - |> (fun sc -> - if cOffset > shownCycles - 1 then - sc + cOffset - shownCycles + 1 - elif cOffset < 0 then - (sc + cOffset) - else - sc) - // final limits check so no cycle is outside allowed range - |> min (Constants.maxLastClk / wsModel.CycleMultiplier - shownCycles) - |> max 0 - - - dispatch <| GenerateWaveforms { - wsModel with - ShownCycles = shownCycles; - StartCycle = startCycle - } - |> TimeHelpers.instrumentInterval "changeZoom" start - -/// Click on these buttons to change the number of visible clock cycles. -let zoomButtons (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElement = - div [ clkCycleButtonStyle ] - [ - button [ Button.Props [clkCycleLeftStyle] ] - (fun _ -> changeZoom wsModel false dispatch) - zoomOutSVG - button [ Button.Props [clkCycleRightStyle] ] - (fun _ -> changeZoom wsModel true dispatch) - zoomInSVG - ] - -/// Click on these to change the highlighted clock cycle. -let clkCycleButtons (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElement = - /// Controls the number of cycles moved by the "◀◀" and "▶▶" buttons - let mult = wsModel.CycleMultiplier - let bigStepSize = max (2*mult) (wsModel.ShownCycles*mult / 2) - - let scrollWaveformsBy (numCycles: int) = - setClkCycle wsModel dispatch (wsModel.CurrClkCycleDetail + numCycles) - - div [ clkCycleButtonStyle ] - [ - // Move left by bigStepSize cycles - button [ Button.Props [clkCycleLeftStyle] ] - (fun _ -> scrollWaveformsBy -bigStepSize) - (str "◀◀") - - // Move left by one cycle - button [ Button.Props [clkCycleInnerStyle] ] - (fun _ -> scrollWaveformsBy -1) - (str "◀") - - // Text input box for manual selection of clock cycle - Input.number [ - Input.Props clkCycleInputProps - - Input.Value ( - match wsModel.ClkCycleBoxIsEmpty with - | true -> "" - | false -> string (wsModel.CurrClkCycleDetail) - ) - // TODO: Test more properly with invalid inputs (including negative numbers) - Input.OnChange(fun c -> - match System.Int32.TryParse c.Value with - | true, n -> - setClkCycle wsModel dispatch n - | false, _ when c.Value = "" -> - dispatch <| SetWSModel {wsModel with ClkCycleBoxIsEmpty = true} - | _ -> - dispatch <| SetWSModel {wsModel with ClkCycleBoxIsEmpty = false} - ) - ] - - // Move right by one cycle - button [ Button.Props [clkCycleInnerStyle] ] - (fun _ -> scrollWaveformsBy 1) - (str "▶") - - // Move right by bigStepSize cycles - button [ Button.Props [clkCycleRightStyle] ] - (fun _ -> scrollWaveformsBy bigStepSize) - (str "▶▶") - ] - -/// ReactElement of the tabs for changing displayed radix -let private radixButtons (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElement = - let radixString = [ - Bin, "Bin" - Hex, "Hex" - Dec, "uDec" - SDec, "sDec" - ] - - let radixTab (radix, radixStr) = - Tabs.tab [ - Tabs.Tab.IsActive(wsModel.Radix = radix) - Tabs.Tab.Props radixTabProps - ] [ a [ - radixTabAStyle - OnClick(fun _ -> dispatch <| GenerateWaveforms {wsModel with Radix = radix}) - ] [ str radixStr ] - ] - - Tabs.tabs [ - Tabs.IsToggle - Tabs.Props [ radixTabsStyle ] - ] (List.map (radixTab) radixString) - - -let highlightCircuit fs comps wave (dispatch: Msg -> Unit) = - dispatch <| Sheet (SheetT.Msg.Wire (BusWireT.Msg.Symbol (SymbolT.SelectSymbols comps))) - // Filter out any non-existent wires - let conns = connsOfWave fs wave - dispatch <| Sheet (SheetT.Msg.SelectWires conns) - -/// Create label of waveform name for each selected wave. -/// Note that this is generated after calling selectedWaves. Any changes to this function -/// must also be made to valueRows and waveRows, as the order of the waves matters here. -/// This is because the wave viewer is comprised of three columns of many rows, rather -/// than many rows of three columns. -let nameRows (model: Model) (wsModel: WaveSimModel) dispatch: ReactElement list = - selectedWaves wsModel - |> List.map (fun wave -> - let visibility = - if wsModel.HoveredLabel = Some wave.WaveId then - "visible" - else "hidden" - - Level.level [ - Level.Level.Option.Props [ - nameRowLevelStyle (wsModel.HoveredLabel = Some wave.WaveId) - let execWithModel (f: Model -> Unit) = ExecFuncInMessage((fun model _ -> f model), dispatch) - OnMouseOver (fun _ -> dispatch <| execWithModel (fun model -> - if wsModel.DraggedIndex = None then - dispatch <| SetWSModel {wsModel with HoveredLabel = Some wave.WaveId} - // Check if symbol exists on Canvas - let symbols = model.Sheet.Wire.Symbol.Symbols - match Map.tryFind (fst wave.WaveId.Id) symbols with - | Some {Component={Type=IOLabel;Label=lab}} -> - let labelComps = - symbols - |> Map.toList - |> List.map (fun (_,sym) -> sym.Component) - |> List.filter (function | {Type=IOLabel;Label = lab'} when lab' = lab -> true |_ -> false) - |> List.map (fun comp -> ComponentId comp.Id) - highlightCircuit wsModel.FastSim labelComps wave dispatch - | Some sym -> - highlightCircuit wsModel.FastSim [fst wave.WaveId.Id] wave dispatch - | None -> ()) - - ) - OnMouseOut (fun _ -> - dispatch <| SetWSModel {wsModel with HoveredLabel = None} - dispatch <| Sheet (SheetT.Msg.Wire (BusWireT.Msg.Symbol (SymbolT.SelectSymbols []))) - dispatch <| Sheet (SheetT.Msg.UpdateSelectedWires (connsOfWave wsModel.FastSim wave, false)) - ) - - Draggable true - - OnDragStart (fun ev -> - ev.dataTransfer.effectAllowed <- "move" - ev.dataTransfer.dropEffect <- "move" - dispatch <| SetWSModel { - wsModel with - DraggedIndex = Some wave.WaveId - PrevSelectedWaves = Some wsModel.SelectedWaves - } - ) - - OnDrag (fun ev -> - ev.dataTransfer.dropEffect <- "move" - let nameColEl = Browser.Dom.document.getElementById "namesColumn" - let bcr = nameColEl.getBoundingClientRect () - - // If the user drags the label outside the bounds of the wave name column - if ev.clientX < bcr.left || ev.clientX > bcr.right || - ev.clientY < bcr.top || ev.clientY > bcr.bottom - then - dispatch <| SetWSModel { - wsModel with - HoveredLabel = Some wave.WaveId - // Use wsModel.SelectedValues if somehow PrevSelectedWaves not set - SelectedWaves = Option.defaultValue wsModel.SelectedWaves wsModel.PrevSelectedWaves - } - ) - - OnDragOver (fun ev -> ev.preventDefault ()) - - OnDragEnter (fun ev -> - ev.preventDefault () - ev.dataTransfer.dropEffect <- "move" - let nameColEl = Browser.Dom.document.getElementById "namesColumn" - let bcr = nameColEl.getBoundingClientRect () - let index = int (ev.clientY - bcr.top) / Constants.rowHeight - 1 - let draggedWave = - match wsModel.DraggedIndex with - | Some waveId -> [waveId] - | None -> [] - - let selectedWaves = - wsModel.SelectedWaves - |> List.except draggedWave - |> List.insertManyAt index draggedWave - - dispatch <| SetWSModel {wsModel with SelectedWaves = selectedWaves} - ) - - OnDragEnd (fun _ -> - dispatch <| SetWSModel { - wsModel with - DraggedIndex = None - PrevSelectedWaves = None - } - ) - ] - ] [ Level.left - [ Props (nameRowLevelLeftProps visibility) ] - [ Delete.delete [ - Delete.Option.Size IsSmall - Delete.Option.Props [ - OnClick (fun _ -> - let selectedWaves = List.except [wave.WaveId] wsModel.SelectedWaves - dispatch <| SetWSModel {wsModel with SelectedWaves = selectedWaves} - ) - ] - ] [] - ] - Level.right - [ Props [ Style [ PaddingRight Constants.labelPadding ] ] ] - [ label [ nameLabelStyle (wsModel.HoveredLabel = Some wave.WaveId) ] [ wave.ViewerDisplayName|> str ] ] - ] - ) - -/// Create column of waveform names -let namesColumn model wsModel dispatch : ReactElement = - let start = TimeHelpers.getTimeMs () - let rows = - nameRows model wsModel dispatch - div (namesColumnProps wsModel) - (List.concat [ topRow []; rows ]) - |> TimeHelpers.instrumentInterval "namesColumn" start - - -/// Create label of waveform value for each selected wave at a given clk cycle. -/// Note that this is generated after calling selectedWaves. -/// Any changes to this function must also be made to nameRows -/// and waveRows, as the order of the waves matters here. This is -/// because the wave viewer is comprised of three columns of many -/// rows, rather than many rows of three columns. -/// Return required width of values column in pixels, and list of cloumn react elements. -let valueRows (wsModel: WaveSimModel) = - let valueColWidth, valueColNumChars = - valuesColumnSize wsModel - selectedWaves wsModel - |> List.map (fun wave -> getWaveValue wsModel.CurrClkCycleDetail wave wave.Width) - |> List.map (fun fd -> - match fd.Width, fd.Dat with - | 1, Word b -> $" {b}" - | _ -> fastDataToPaddedString valueColNumChars wsModel.Radix fd) - |> List.map (fun value -> label [ valueLabelStyle ] [ str value ]) - |> (fun rows -> valueColWidth, rows) - - -/// Generate a row of numbers in the waveforms column. -/// Numbers correspond to clock cycles multiplied by the current multiplier -let clkCycleNumberRow (wsModel: WaveSimModel) = - let makeClkCycleLabel i = - let n = i * wsModel.CycleMultiplier - match singleWaveWidth wsModel with - | width when width < float Constants.clkCycleNarrowThreshold && i % 5 <> 0 -> - [] - | width when n >= 1000 && width < (float Constants.clkCycleNarrowThreshold * 4. / 3.) && i % 10 <> 0 -> - [] - | _ -> - [ text (clkCycleText wsModel i) [str (string n)] ] - - - [ wsModel.StartCycle .. endCycle wsModel] - |> List.collect makeClkCycleLabel - |> svg (clkCycleNumberRowProps wsModel) - -/// Create column of waveform values -let private valuesColumn wsModel : ReactElement = - let start = TimeHelpers.getTimeMs () - let width, rows = valueRows wsModel - let cursorClkNum = wsModel.CurrClkCycleDetail - let topRowNumber = [ text [Style [FontWeight "bold"; PaddingLeft "2pt"]] [str (string <| cursorClkNum)] ] - - div [ HTMLAttr.Id "ValuesCol" ; valuesColumnStyle width] - (List.concat [ topRow topRowNumber ; rows ]) - |> TimeHelpers.instrumentInterval "valuesColumn" start - -/// Generate a column of waveforms corresponding to selected waves. -let waveformColumn (wsModel: WaveSimModel) dispatch : ReactElement = - let start = TimeHelpers.getTimeMs () - /// Note that this is generated after calling selectedWaves. - /// Any changes to this function must also be made to nameRows - /// and valueRows, as the order of the waves matters here. This is - /// because the wave viewer is comprised of three columns of many - /// rows, rather than many rows of three columns. - let waves = selectedWaves wsModel - if List.exists (fun wave -> wave.SVG = None) waves then - dispatch <| GenerateCurrentWaveforms - let waveRows : ReactElement list = - waves - |> List.map (fun wave -> - match wave.SVG with - | Some waveform -> - waveform - | None -> - div [] [] // the GenerateCurrentWaveforms message will soon update this - ) - - div [ waveformColumnStyle ] - [ - clkCycleHighlightSVG wsModel dispatch - div [ waveRowsStyle <| wsModel.WaveformColumnWidth] - ([ clkCycleNumberRow wsModel ] @ - waveRows - ) - ] - |> TimeHelpers.instrumentInterval "waveformColumn" start - -/// Display the names, waveforms, and values of selected waveforms -let showWaveforms (model: Model) (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElement = - if List.isEmpty wsModel.SelectedWaves then - div [] [] // no waveforms - else - let wHeight = calcWaveformHeight wsModel - let fixedHeight = Constants.softScrollBarWidth + Constants.topHalfHeight - let cssHeight = - if wsModel.SelectedRams.Count > 0 then - $"min( calc(50vh - (0.5 * {fixedHeight}px)) , {wHeight}px)" - else - $"min( calc(100vh - {fixedHeight}px) , {wHeight}px)" - - div [ HTMLAttr.Id "Scroller"; Style [ Height cssHeight; Width "100%"; CSSProp.Custom("overflow", "auto")]] [ - div [ HTMLAttr.Id "WaveCols" ;showWaveformsStyle ] - [ - namesColumn model wsModel dispatch - waveformColumn wsModel dispatch - valuesColumn wsModel - ] - ] - -/// Table row that shows the address and data of a RAM component. -let ramTableRow ((addr, data,rowType): string * string * RamRowType): ReactElement = - - tr [ Style <| ramTableRowStyle rowType ] [ - td [] [ str addr ] - td [] [ str data ] - ] - -/// Table showing contents of a RAM component. -let ramTable (wsModel: WaveSimModel) ((ramId, ramLabel): FComponentId * string) : ReactElement = - let wanted = calcWaveformAndScrollBarHeight wsModel - let maxHeight = max (screenHeight() - (min wanted (screenHeight()/2.)) - 300.) 30. - let fs = wsModel.FastSim - match Map.tryFind ramId wsModel.FastSim.FComps with - | None -> div [] [] - | Some fc -> - let step = wsModel.CurrClkCycle - FastRun.runFastSimulation None step fs |> ignore // not sure why this is needed - - // in some cases fast sim is run for one cycle less than currClockCycle - let memData = - match fc.FType with - | ROM1 mem - | AsyncROM1 mem -> mem - | RAM1 mem - | AsyncRAM1 mem -> - match FastRun.extractFastSimulationState fs wsModel.CurrClkCycle ramId with - |RamState mem -> mem - | x -> failwithf $"What? Unexpected state {x} from cycle {wsModel.CurrClkCycle} \ - in RAM component '{ramLabel}'. FastSim step = {fs.ClockTick}" - | _ -> failwithf $"Given a component {fc.FType} which is not a vaild RAM" - let aWidth,dWidth = memData.AddressWidth,memData.WordWidth - - let print w (a:int64) = NumberHelpers.valToPaddedString w wsModel.Radix (((1L <<< w) - 1L) &&& a) - - let lastLocation = int64 ((2 <<< memData.AddressWidth - 1) - 1) - - /// print a single 0 location as one table row - let print1 (a:int64,b:int64,rw:RamRowType) = $"{print aWidth a}",$"{print dWidth b}",rw - /// print a range of zero locations as one table row - - let print2 (a1:int64) (a2:int64) (d:int64) = $"{print aWidth (a1+1L)} ... {print aWidth (a2-1L)}", $"{print dWidth d}",RAMNormal - - /// output info for one table row filling the given zero memory gap or arbitrary size, or no line if there is no gap. - let printGap (gStart:int64) (gEnd:int64) = - match gEnd - gStart with - | 1L -> [] - | 2L -> [print1 ((gEnd + gStart) / 2L, 0L,RAMNormal)] - | n when n > 2L -> - [print2 gStart gEnd 0L] - | _ -> - failwithf $"What? gEnd={gEnd},gStart={gStart}: negative or zero gaps are impossible..." - - /// transform Sparse RAM info into strings to print in a table, adding extra lines for zero gaps - /// line styling is controlled by a RamRowtype value and added later when the table row react is generated - let addGapLines (items: (int64*int64*RamRowType) list) = - let startItem = - match items[0] with - | -1L,_,_ -> [] - | gStart,dStart,rw-> [print1 (gStart,dStart,rw)] - List.pairwise items - |> List.collect (fun ((gStart,_,_),(gEnd,dEnd,rwe)) -> - let thisItem = if gEnd = lastLocation + 1L then [] else [print1 (gEnd,dEnd,rwe)] - [printGap gStart gEnd; thisItem]) - |> List.concat - - /// Add a RAMNormal RamRowType value to every location in mem. - /// Add in additional locations for read and/or write if needed. - /// Set RamRowValue type to RAMWritten or RAMRead for thse locations. - /// Write is always 1 cycle after WEN=1 and address. - /// Read is 1 (0) cycles after address for sync (asynch) memories. - let addReadWrite (fc:FastComponent) (step:int) (mem: Map) = - let getInt64 (a: IOArray) step = - let w = a.Width - match w with - | w when w > 32 -> int64 <| convertBigIntToUInt64 w a.BigIntStep[step] - | _ -> int64 <| a.UInt32Step[step] - - let readStep = - match fc.FType with - | AsyncROM1 _ | AsyncRAM1 _ -> step - | ROM1 _ | RAM1 _ -> step - 1 - | _ -> failwithf $"What? {fc.FullName} should be a memory component" - - let addrSteps step = getInt64 fc.InputLinks[0] step - - let readOpt = - match step, fc.FType with - | 0,ROM1 _ | 0, RAM1 _ -> None - | _ -> - addrSteps readStep - |> Some - let writeOpt = - match step, fc.FType with - | _, ROM1 _ - | _, AsyncROM1 _ - | 0, _ -> None - | _, RAM1 _ | _, AsyncRAM1 _ when getInt64 fc.InputLinks[2] (step-1) = 1L -> - addrSteps (step-1) - |> Some - | _ -> - None - - /// Mark addr in memory map as being rType - /// if addr does not exist - create it - let addToMap rType addr mem:Map = - match Map.tryFind addr mem with - | Some (d,_) -> Map.add addr (d,rType) mem - | None -> Map.add addr (0L,rType) mem - - - Map.map (fun k v -> v,RAMNormal) mem - |> (fun mem -> - match readOpt with - | Some addr -> addToMap RAMRead addr mem - | None -> mem - |> (fun mem -> - match writeOpt with // overwrite RAMRead here is need be - | Some addr -> addToMap RAMWritten addr mem - | None -> mem)) - - - /// add fake locations beyong normal address range so that - /// addGapLines fills these (if need be). These locations are then removed - let addEndPoints (items:(int64*int64*RamRowType) list) = - let ad (a,d,rw) = a - match items.Length with - | 0 -> [-1L,0L,RAMNormal; lastLocation,0L,RAMNormal] - | _ -> - if ad items[0] < 0L then items else List.insertAt 0 (-1L,-1L,RAMNormal) items - |> (fun items -> - if ad items[items.Length-1] = lastLocation then - items else - List.insertAt items.Length (lastLocation+1L,0L,RAMNormal) items) - - - let lineItems = - memData.Data - |> addReadWrite fc step - |> Map.toList - |> List.map (fun (a,(d,rw)) -> a,d,rw) - |> List.filter (fun (a,d,rw) -> d<>0L || rw <> RAMNormal) - |> List.sort - |> addEndPoints - |> addGapLines - - - - Level.item [ - Level.Item.Option.Props ramTableLevelProps - Level.Item.Option.HasTextCentered - ] [ - Heading.h6 [ - Heading.Option.Props [ centerAlignStyle ] - ] [ str ramLabel ] - div [Style [MaxHeight maxHeight;OverflowY OverflowOptions.Auto]] [ - Table.table [ - Table.IsFullWidth - Table.IsBordered - ] [ thead [] [ - tr [] [ - th [ centerAlignStyle ] [ str "Address"] - th [ centerAlignStyle ] [ str "Data"; sub [Style [MarginLeft "2px"; FontSize "10px"]] [str (string wsModel.CurrClkCycle)]] - ] - ] - tbody [] - (List.map ramTableRow lineItems) - ] ] - br [] - ] - -/// Bulma Level component of tables showing RAM contents. -let ramTables (wsModel: WaveSimModel) : ReactElement = - let inlineStyle (styles:CSSProp list) = div [Style (Display DisplayOptions.Inline :: styles)] - let start = TimeHelpers.getTimeMs () - let selectedRams = Map.toList wsModel.SelectedRams - if List.length selectedRams > 0 then - let tables = - let headerRow = - ["read", RAMRead; "overwritten",RAMWritten] - |> List.map (fun (op, opStyle) -> inlineStyle [Margin "0px"] [inlineStyle (ramTableRowStyle opStyle) [str op]]) - |> function - | [a;b] -> [str "Key: Memory location is " ; a; str ", or " ;b; str ". Click waveforms or use cursor control to change current cycle."] - | _ -> failwithf "What? Can't happen!" - List.map (fun ram -> td [Style [BorderColor "white"]] [ramTable wsModel ram]) selectedRams - |> (fun tables -> [tbody [] [tr [] [th [ColSpan selectedRams.Length] [inlineStyle [] headerRow]]; tr [Style [Border "10px"]] tables]]) - |> Fulma.Table.table [ - Table.TableOption.Props ramTablesLevelProps; - Table.IsFullWidth; - Table.IsBordered; - ] - div [HTMLAttr.Id "TablesDiv"] [ hr [ Style [ Margin "5px"]]; br [ Style [ Margin "0px"]]; tables] - else div [] [] - |> TimeHelpers.instrumentInterval "ramTables" start - -/// This function regenerates all the waveforms listed on wavesToBeMade . -/// Generation is subject to timeout, so may not complete. -/// This function have been augmented with performance monitoring function, turn Constants.showPerfLogs -/// to print performance information to console. -/// A tuple with the following information:
-/// a) allWaves (with new waveforms),
-/// b) numberDone (no of waveforms made), and
-/// c) timeToDo (Some timeTaken when greater than timeOut or None -/// if completed with no time out).
-let makeWaveformsWithTimeOut - (timeOut: option) - (ws: WaveSimModel) - (allWaves: Map) - (wavesToBeMade: list) - : Map * int * option = - let start = TimeHelpers.getTimeMs() - let allWaves, numberDone, timeToDo = - ((allWaves, 0, None), wavesToBeMade) - ||> List.fold (fun (all,n, _) wi -> - match timeOut, TimeHelpers.getTimeMs() - start with - | Some timeOut, timeSoFar when timeOut < timeSoFar -> - all, n, Some timeSoFar - | _ -> - (Map.change wi (Option.map (generateWaveform ws wi)) all), n+1, None) - let finish = TimeHelpers.getTimeMs() - if Constants.showPerfLogs then - let countWavesWithWidthRange lowerLim upperLim = - wavesToBeMade - |> List.map (fun wi -> (Map.find wi allWaves).Width) - |> List.filter (fun width -> lowerLim <= width && width <= upperLim) - |> List.length - - printfn "PERF:makeWaveformsWithTimeOut: generating visible only: %b" Constants.generateVisibleOnly - printfn "PERF:makeWaveformsWithTimeOut: making %d/%d waveforms" (List.length wavesToBeMade) (Map.count allWaves) - printfn "PERF:makeWaveformsWithTimeOut: binary = %d" (countWavesWithWidthRange 1 1) - printfn "PERF:makeWaveformsWithTimeOut: int32 = %d" (countWavesWithWidthRange 2 32) - printfn "PERF:makeWaveformsWithTimeOut: process took %.2fms" (finish-start) - - allWaves, numberDone, timeToDo - -/// Generate scrollbar SVG info based on current WaveSimModel. -/// Called in refreshWaveSim after WaveSimModel has been changed. -/// Target WaveSimModel. -/// Anonymous record contaning the information to be updated: thumb width, -/// thumb position, and number of cycles the background represents. -/// Note: bkg = background; tb = thumb. -let generateScrollbarInfo (wsm: WaveSimModel): {| tbWidth: float; tbPos: float; bkgRep: int |} = - let mult = wsm.CycleMultiplier - let bkgWidth = wsm.ScrollbarBkgWidth - 60. // 60 = 2x width of buttons - - /// Return target value when within min and max value, otherwise min or max. - let bound (minV: int) (maxV: int) (tarV: int): int = tarV |> max minV |> min maxV - let currShownMaxCyc = wsm.StartCycle + wsm.ShownCycles - let newBkgRep = [ wsm.ScrollbarBkgRepCycs; currShownMaxCyc; wsm.ShownCycles*2 ] |> List.max |> bound 0 (Constants.maxLastClk / mult) - - let tbCalcWidth = bkgWidth / (1. + (float newBkgRep / float wsm.ShownCycles)) - let tbWidth = max tbCalcWidth WaveSimStyle.Constants.scrollbarThumbMinWidth - - let tbMoveWidth = bkgWidth - tbWidth - let tbPos = (float wsm.StartCycle) / (float newBkgRep - float wsm.ShownCycles) * tbMoveWidth - - // debug statements: - // printfn "DEBUG:generateScrollbarInfo: Input -" - // printfn "DEBUG:generateScrollbarInfo: wsm.CurrClkCycle = %d cycles" wsm.CurrClkCycle - // printfn "DEBUG:generateScrollbarInfo: wsm.StartCycle = %d cycles" wsm.StartCycle - // printfn "DEBUG:generateScrollbarInfo: wsm.ShownCycles = %d cycles" wsm.ShownCycles - // printfn "DEBUG:generateScrollbarInfo: wsm.ScrollbarBkgRepCycs = %d cycles" wsm.ScrollbarBkgRepCycs - // printfn "DEBUG:generateScrollbarInfo: bkgWidth = %.1f cycles" bkgWidth - // printfn "DEBUG:generateScrollbarInfo: Output -" - // printfn "DEBUG:generateScrollbarInfo: tbWidth = %.1fpx" tbWidth - // printfn "DEBUG:generateScrollbarInfo: tbPos = %.1fpx" tbPos - // printfn "DEBUG:generateScrollbarInfo: newBkgRep = %d cycles" newBkgRep - - {| tbWidth = tbWidth; tbPos = tbPos; bkgRep = newBkgRep |} - -/// Make scrollbar element based on information in WaveSimModel. -/// Called in viewWaveSim, presumably after refreshWaveSim was called. -/// Target WaveSimModel. -/// Dispatch function to send messages with. Not used directly, but passed to tbMouseMoveOp. -/// React element to be placed in to DOM. -let makeScrollbar (wsm: WaveSimModel) (dispatch: Msg->unit): ReactElement = - // button props - let scrollWaveformViewBy (numCycles: float) = setScrollbarTbByCycs wsm dispatch numCycles - - // svg props - let bkgWidth = wsm.ScrollbarBkgWidth - 60. // 60 = 2x width of buttons - - let tbMouseDownHandler (event: Browser.Types.MouseEvent): unit = // start drag - ScrollbarMouseMsg (event.clientX, StartScrollbarDrag, dispatch) |> dispatch - - let tbMouseMoveHandler (event: Browser.Types.MouseEvent): unit = // if in drag, drag; otherwise do nothing - if Option.isSome wsm.ScrollbarTbOffset - then ScrollbarMouseMsg (event.clientX, InScrollbarDrag, dispatch) |> dispatch - else () - - let tbMouseUpHandler (event: Browser.Types.MouseEvent): unit = // if in drag, clear drag; otherwise do nothing - if Option.isSome wsm.ScrollbarTbOffset - then ScrollbarMouseMsg (event.clientX, ClearScrollbarDrag, dispatch) |> dispatch - else () - - let bkgPropList (width: float): List = - [ - HTMLAttr.Id "scrollbarThumb"; - SVGAttr.X $"0px"; SVGAttr.Y "0.5px"; - SVGAttr.Width $"%.1f{width}px"; SVGAttr.Height $"%.1f{WaveSimStyle.Constants.softScrollBarWidth-1.0}px"; - SVGAttr.Fill "white"; SVGAttr.Stroke "gray"; SVGAttr.StrokeWidth "1px"; - ] - - let tbPropList (pos: float) (width: float): List = - [ - HTMLAttr.Id "scrollbarBkg"; - Style [ Cursor "grab"]; - SVGAttr.X $"%.1f{pos}px"; SVGAttr.Y "0.5px"; - SVGAttr.Width $"%.1f{width}px"; SVGAttr.Height $"%.1f{WaveSimStyle.Constants.softScrollBarWidth-1.0}px"; - SVGAttr.Fill "lightgrey"; SVGAttr.Stroke "gray"; SVGAttr.StrokeWidth "1px"; - OnMouseDown tbMouseDownHandler; OnMouseUp tbMouseUpHandler; OnMouseMove tbMouseMoveHandler; - ] - - div [ Style [ MarginTop "5px"; MarginBottom "5px"; Height "25px"]] [ - button [ Button.Props [scrollbarClkCycleLeftStyle] ] - (fun _ -> scrollWaveformViewBy -1.0) - (str "◀") - svg - [Style [Width $"{bkgWidth}"; Height $"{WaveSimStyle.Constants.softScrollBarWidth}px"];] - [ - rect (bkgPropList bkgWidth) []; // background - rect (tbPropList wsm.ScrollbarTbPos wsm.ScrollbarTbWidth) []; // thumb - ] - button [ Button.Props [scrollbarClkCycleRightStyle]] - (fun _ -> scrollWaveformViewBy 1.0) - (str "▶") - ] - -/// Update waveform view information based on mouse postion in the X direction. -/// Called in update when ScrollbarMouseMsg is dispatched. -/// Target WaveSimModel. -/// Dispatch function to send messages with, not used directly. -/// Cursor postion in relation to the screen, i.e. event.clientX. -/// Scrollbar action to do, see choices for more info, in type of ScrollbarMouseAction. -/// Note that screenX does NOT scale with web zoom and will cause weird results! -let updateScrollbar (wsm: WaveSimModel) (dispatch: Msg->unit) (cursor: float) (action: ScrollbarMouseAction): unit = - /// Translate mouse movements in pixels to number of cycles to move by. - /// Linear translator aims to allow scrollbar thumb to follow cursor. - /// Number of pixels mouse has moved in X direction, obtained from MouseMove event. - /// Number of cycles to move by. - /// Swap this out with some other mouse-to-cycle translator for better user experience. - let linearMouseToCycleTranslator (dx: float): float = - let cycleToPixelRatio = float wsm.ScrollbarBkgRepCycs / (wsm.ScrollbarBkgWidth - wsm.ScrollbarTbWidth) - dx*cycleToPixelRatio - match action with - | StartScrollbarDrag -> // record offset - let offset = Some (wsm.ScrollbarTbPos-cursor) - setScrollbarOffset wsm dispatch offset - | InScrollbarDrag -> // in drag, unknown queue state: update counter, if queue is empty then dispatch ReleaseScrollQueue message - let canDispatch = wsm.ScrollbarQueueIsEmpty - setScrollbarLastX wsm dispatch false - if canDispatch then ScrollbarMouseMsg (cursor, ReleaseScrollQueue, dispatch) |> dispatch - | ReleaseScrollQueue -> // in drag, and queue is clear: update and set ScrollbarQueueIsEmpty to true - match wsm.ScrollbarTbOffset with - | Some puckOffset -> - let dx = puckOffset + cursor - wsm.ScrollbarTbPos // offset + new cursor = new thumb; dx = new thumb - old thumb - setScrollbarTbByCycs wsm dispatch (linearMouseToCycleTranslator dx) - | None -> () - | ClearScrollbarDrag -> // clear offset - setScrollbarOffset wsm dispatch None +open WaveSimWaves.Constants /// Start or update a spinner popup let updateSpinner (name:string) payload (numToDo:int) (model: Model) = @@ -1151,7 +84,7 @@ let rec refreshWaveSim (newSimulation: bool) (wsModel: WaveSimModel) (model: Mod let allWaves = if newSimulation then //printfn "making new waves..." - getWaves wsModel fs + WaveSimWaves.getWaves wsModel fs else wsModel.AllWaves let model = updateWSModel (fun ws -> {ws with AllWaves = allWaves}) model // redo viewer width (and therefore shown cycles etc) based on selected waves names @@ -1172,7 +105,7 @@ let rec refreshWaveSim (newSimulation: bool) (wsModel: WaveSimModel) (model: Mod |> Map.filter (fun wi wave -> // Only generate waveforms for selected waves. // Regenerate waveforms whenever they have changed - let hasChanged = not <| waveformIsUptodate wsModel wave + let hasChanged = not <| WaveSimWaves.waveformIsUptodate wsModel wave //if List.contains index ws.SelectedWaves then List.exists (fun wi' -> isSameWave wi wi') wsModel.SelectedWaves && hasChanged && simulationIsUptodate) |> Map.toList @@ -1181,7 +114,7 @@ let rec refreshWaveSim (newSimulation: bool) (wsModel: WaveSimModel) (model: Mod let model, allWaves, spinnerPayload, numToDo = //printfn $"{wavesToBeMade.Length} waves to make." let numToDo = wavesToBeMade.Length - makeWaveformsWithTimeOut (Some Constants.initSimulationTime) wsModel allWaves wavesToBeMade + WaveSimWaves.makeWaveformsWithTimeOut (Some Constants.initSimulationTime) wsModel allWaves wavesToBeMade |> (fun (allWaves, numDone, timeOpt) -> match wavesToBeMade.Length - numDone, timeOpt with | n, None -> @@ -1190,7 +123,7 @@ let rec refreshWaveSim (newSimulation: bool) (wsModel: WaveSimModel) (model: Mod failwithf "What? makewaveformsWithTimeOut must make at least one waveform" | numToDo, Some t when float wavesToBeMade.Length * t / float numDone < Constants.maxSimulationTimeWithoutSpinner -> - let (allWaves, numDone, timeOpt) = makeWaveformsWithTimeOut None wsModel allWaves wavesToBeMade + let (allWaves, numDone, timeOpt) = WaveSimWaves.makeWaveformsWithTimeOut None wsModel allWaves wavesToBeMade model, allWaves, None, numToDo - numDone | numToDo, _ -> let payload = Some ("Making waves", refreshWaveSim false {wsModel with AllWaves = allWaves} >> fst) @@ -1228,10 +161,7 @@ let rec refreshWaveSim (newSimulation: bool) (wsModel: WaveSimModel) (model: Mod RamComps = ramComps SelectedRams = selectedRams FastSim = fs - ScrollbarTbWidth = scrollbarInfo.tbWidth - ScrollbarTbPos = scrollbarInfo.tbPos - ScrollbarBkgRepCycs = scrollbarInfo.bkgRep - } + } |> validateScrollBarInfo let model = match spinnerPayload with @@ -1376,7 +306,7 @@ let topHalf canvasState (model: Model) dispatch : ReactElement * bool = multiplierMenuButton wsModel dispatch - radixButtons wsModel dispatch + WaveSimWaveforms.radixButtons wsModel dispatch clkCycleButtons wsModel dispatch ] @@ -1389,7 +319,9 @@ let topHalf canvasState (model: Model) dispatch : ReactElement * bool = div [Style [MarginTop 20.; Display DisplayOptions.Flex; JustifyContent "space-between"]] [ refreshStartEndButton() - div [Style [inlineNoWrap; Flex "0 1"]] [selectWavesButton wsModel dispatch; selectRamButton wsModel dispatch] + div [Style [inlineNoWrap; Flex "0 1"]] [ + WaveSimSelect.selectWavesButton wsModel dispatch + WaveSimSelect.selectRamButton wsModel dispatch] ] messageOrControlLine], needsBottomHalf @@ -1406,16 +338,16 @@ let viewWaveSim canvasState (model: Model) dispatch : ReactElement = let bottomHalf = // this has fixed height div [HTMLAttr.Id "BottomHalf" ; showWaveformsAndRamStyle (if needsRAMs then screenHeight() else height)] ( if wsModel.SelectedWaves.Length > 0 then [ - showWaveforms model wsModel dispatch + WaveSimWaveforms.showWaveforms model wsModel dispatch makeScrollbar wsModel dispatch ] else [] @ - [ramTables wsModel] + [WaveSimRams.ramTables wsModel] ) div [] [ - selectRamModal wsModel dispatch - selectWavesModal wsModel dispatch + WaveSimSelect.selectRamModal wsModel dispatch + WaveSimSelect.selectWavesModal wsModel dispatch div [ viewWaveSimStyle ] [ //printfn $"WSmodel state: {wsModel.State}" diff --git a/src/Renderer/UI/WaveSim/WaveSimHelpers.fs b/src/Renderer/UI/WaveSim/WaveSimHelpers.fs index def059492..7a634c305 100644 --- a/src/Renderer/UI/WaveSim/WaveSimHelpers.fs +++ b/src/Renderer/UI/WaveSim/WaveSimHelpers.fs @@ -14,57 +14,9 @@ open FastRun open NumberHelpers +open WaveSimStyle -module Constants = - - /// initial time running simulation without spinner to check speed (in ms) - let initSimulationTime = 100. - /// max estimated time to run simulation and not need a spinner (in ms) - let maxSimulationTimeWithoutSpinner = 200. - /// The horizontal length of a transition cross-hatch for non-binary waveforms - let nonBinaryTransLen : float = 2. - - /// The height of the viewbox used for a wave's SVG. This is the same as the height - /// of a label in the name and value columns. - /// TODO: Combine this with WaveSimStyle.Constants.rowHeight? - let viewBoxHeight : float = 30.0 - - /// Height of a waveform - let waveHeight : float = 0.8 * viewBoxHeight - /// Vertical padding between top and bottom of each wave and the row it is in. - let spacing : float = (viewBoxHeight - waveHeight) / 2. - - /// y-coordinate of the top of a waveform - let yTop = spacing - /// y-coordiante of the bottom of a waveform - let yBot = waveHeight + spacing - - /// minium number of cycles on screen when zooming in - let minVisibleCycles = 3 - - /// Minimum number of visible clock cycles. - let minCycleWidth = 5 - - let zoomChangeFactor = 1.5 - - /// If the width of a non-binary waveform is less than this value, display a cross-hatch - /// to indicate a non-binary wave is rapidly changing value. - let clkCycleNarrowThreshold = 20 - - /// number of extra steps simulated beyond that used in simulation. Is this needed? - let extraSimulatedSteps = 5 - - let infoMessage = - "Find ports by any part of their name. '.' = show all. '*' = show selected. '-' = collapse all" - - let outOfDateMessage = "Use refresh button to update waveforms. 'End' and then 'Start' to simulate a different sheet" - - let infoSignUnicode = "\U0001F6C8" - - let waveLegendMaxChars = 35 - let valueColumnMaxChars = 35 - let multipliers = [1;2;5;10;20;50] @@ -76,15 +28,6 @@ module Constants = let subSamp (arr: 'T array) (start:int) (count: int) (mult:int) = Array.init count (fun n -> arr[start + n*mult]) -// maybe these should be defined earlier in compile order? Or added as list functions? - -let listMaxWithDef defaultValue lst = - defaultValue :: lst - |> List.max - -let listCollectSomes mapFn lst = - lst - |> List.collect (fun x -> match mapFn x with | Some r -> [r] | None -> []) /// Determines whether a clock cycle is generated with a vertical bar at the beginning, /// denoting that a waveform changes value at the start of that clock cycle. NB this @@ -115,29 +58,6 @@ type Gap = { Length: int } -let rec validateSimParas (ws: WaveSimModel) = - if ws.StartCycle < 0 then - printfn $"ERROR in Sim parameters: StartCycle {ws.StartCycle} < 0" - validateSimParas {ws with StartCycle = 0} - elif (ws.StartCycle + ws.ShownCycles-1)*ws.CycleMultiplier > Constants.maxLastClk then - printfn $"Correcting sim paras by reducing StartCycle" - {ws with StartCycle = Constants.maxLastClk / ws.CycleMultiplier - ws.ShownCycles} - elif ws.CurrClkCycle < ws.StartCycle || ws.CurrClkCycle >= ws.StartCycle + ws.ShownCycles then - printfn $"Resetting CurClkCycle which {ws.CurrClkCycle} was too large with multiplier = {ws.CycleMultiplier}" - {ws with CurrClkCycle = ws.StartCycle; CurrClkCycleDetail = ws.StartCycle*ws.CycleMultiplier} - else ws - -let changeMultiplier newMultiplier (ws: WaveSimModel) = - let oldM = ws.CycleMultiplier - printfn $"Old: {oldM} shown {ws.ShownCycles} start={ws.StartCycle} NewM={newMultiplier}" - let sampsHalf = (float ws.ShownCycles - 1.) / 2. - let newShown = min ws.ShownCycles (Constants.maxLastClk / newMultiplier) - let newStart = int ((float ws.StartCycle + sampsHalf) * float oldM / float newMultiplier - (float newShown - 1.) / 2.) - printfn $"New: shown={newShown} start = {newStart}" - {ws with ShownCycles = newShown; StartCycle = newStart; CycleMultiplier = newMultiplier} - |> validateSimParas - - @@ -151,32 +71,12 @@ let xShift clkCycleWidth = else Constants.nonBinaryTransLen -/// Width of one clock cycle. -let singleWaveWidth m = max 5.0 (float m.WaveformColumnWidth / float m.ShownCycles) - -/// Left-most coordinate of the SVG viewbox. -let viewBoxMinX m = string (float m.StartCycle * singleWaveWidth m) - -/// Total width of the SVG viewbox. -let viewBoxWidth m = string (max 5.0 (m.WaveformColumnWidth)) -/// Right-most visible clock cycle. -let endCycle wsModel = wsModel.StartCycle + (wsModel.ShownCycles) - 1 /// Helper function to create Bulma buttons let button options func label = Button.button (List.append options [ Button.OnClick func ]) [ label ] -/// List of selected waves (of type Wave) -let selectedWaves (wsModel: WaveSimModel) : Wave list = - wsModel.SelectedWaves - |> List.map (fun wi -> Map.tryFind wi wsModel.AllWaves |> Option.toList) - |> List.concat -/// Convert XYPos list to string -let pointsToString (points: XYPos array) : string = - Array.fold (fun str (point: XYPos) -> - $"{str} %.1f{point.X},%.1f{point.Y} " - ) "" points /// Retrieve value of wave at given clock cycle as an int. /// At extra (sampling) zoom this allows detail clock cycles within one sample @@ -512,44 +412,7 @@ let wavesToIds (waves: Wave list) = let tr1 react = tr [] [ react ] let td1 react = td [] [ react ] -//---------------------------Code for selector details state----------------------------------// - -// It would be better to do this with one subfunction and Optics! - -/// Sets or clears a subset of ShowSheetDetail -let setWaveSheetSelectionOpen (wsModel: WaveSimModel) (subSheets: string list list) (show: bool) = - let setChange = Set.ofList subSheets - let newSelect = - match show with - | false -> Set.difference wsModel.ShowSheetDetail setChange - | true -> Set.union setChange wsModel.ShowSheetDetail - {wsModel with ShowSheetDetail = newSelect} - -/// Sets or clears a subset of ShowComponentDetail -let setWaveComponentSelectionOpen (wsModel: WaveSimModel) (fIds: FComponentId list) (show: bool) = - let fIdSet = Set.ofList fIds - let newSelect = - match show with - | true -> Set.union fIdSet wsModel.ShowComponentDetail - | false -> Set.difference wsModel.ShowComponentDetail fIdSet - {wsModel with ShowComponentDetail = newSelect} - - -/// Sets or clears a subset of ShowGroupDetail -let setWaveGroupSelectionOpen (wsModel: WaveSimModel) (grps :(ComponentGroup*string list) list) (show: bool) = - let grpSet = Set.ofList grps - let newSelect = - match show with - | true -> Set.union grpSet wsModel.ShowGroupDetail - | false -> Set.difference wsModel.ShowGroupDetail grpSet - {wsModel with ShowGroupDetail = newSelect} - -let setSelectionOpen (wsModel: WaveSimModel) (cBox: CheckBoxStyle) (show:bool) = - match cBox with - | PortItem _ -> failwithf "What? setselectionopen cannot be called from a Port" - | ComponentItem fc -> setWaveComponentSelectionOpen wsModel [fc.fId] show - | GroupItem (grp,subSheet) -> setWaveGroupSelectionOpen wsModel [grp,subSheet] show - | SheetItem subSheet -> setWaveSheetSelectionOpen wsModel [subSheet] show + /// get all waves electrically connected to a given wave diff --git a/src/Renderer/UI/WaveSim/WaveSimNavigation.fs b/src/Renderer/UI/WaveSim/WaveSimNavigation.fs new file mode 100644 index 000000000..e0a3005db --- /dev/null +++ b/src/Renderer/UI/WaveSim/WaveSimNavigation.fs @@ -0,0 +1,464 @@ +module WaveSimNavigation + +open Fulma +open Fulma.Extensions.Wikiki +open Fable.React +open Fable.React.Props + +open CommonTypes +open ModelType +open ModelHelpers +open WaveSimStyle +open WaveSimHelpers +open TopMenuView +open SimulatorTypes +open WaveSimStyle.Constants + +/// Generate scrollbar SVG info based on current WaveSimModel. +/// Called in refreshWaveSim after WaveSimModel has been changed. +/// Target WaveSimModel. +/// Anonymous record contaning the information to be updated: thumb width, +/// thumb position, and number of cycles the background represents. +/// Note: bkg = background; tb = thumb. +let generateScrollbarInfo (wsm: WaveSimModel): {| tbWidth: float; tbPos: float; bkgRep: int |} = + let mult = wsm.CycleMultiplier + let bkgWidth = wsm.ScrollbarBkgWidth - 60. // 60 = 2x width of buttons + + /// Return target value when within min and max value, otherwise min or max. + let bound (minV: int) (maxV: int) (tarV: int): int = tarV |> max minV |> min maxV + let currShownMaxCyc = wsm.StartCycle + wsm.ShownCycles + let newBkgRep = [ wsm.ScrollbarBkgRepCycs; currShownMaxCyc; wsm.ShownCycles*2 ] |> List.max |> bound 0 (Constants.maxLastClk / mult) + + let tbCalcWidth = bkgWidth / (max 1. (float newBkgRep / float wsm.ShownCycles)) + let tbWidth = max tbCalcWidth Constants.scrollbarThumbMinWidth + + let tbMoveWidth = bkgWidth - tbWidth + let tbPos = (float wsm.StartCycle) / (float newBkgRep - float wsm.ShownCycles) * tbMoveWidth + + // debug statements: + // printfn "DEBUG:generateScrollbarInfo: Input -" + // printfn "DEBUG:generateScrollbarInfo: wsm.CurrClkCycle = %d cycles" wsm.CurrClkCycle + // printfn "DEBUG:generateScrollbarInfo: wsm.StartCycle = %d cycles" wsm.StartCycle + // printfn "DEBUG:generateScrollbarInfo: wsm.ShownCycles = %d cycles" wsm.ShownCycles + // printfn "DEBUG:generateScrollbarInfo: wsm.ScrollbarBkgRepCycs = %d cycles" wsm.ScrollbarBkgRepCycs + // printfn "DEBUG:generateScrollbarInfo: bkgWidth = %.1f cycles" bkgWidth + // printfn "DEBUG:generateScrollbarInfo: Output -" + // printfn "DEBUG:generateScrollbarInfo: tbWidth = %.1fpx" tbWidth + // printfn "DEBUG:generateScrollbarInfo: tbPos = %.1fpx" tbPos + // printfn "DEBUG:generateScrollbarInfo: newBkgRep = %d cycles" newBkgRep + + {| tbWidth = tbWidth; tbPos = tbPos; bkgRep = newBkgRep |} + +/// Make scrollbar parameters consistent with changed zoom. This asumes the scrollbar +/// width has not changed, because that can only be calculated from viewer width in model. +let validateScrollBarInfo (wsm: WaveSimModel) = + let scrollInfo = generateScrollbarInfo wsm + {wsm with ScrollbarTbPos = scrollInfo.tbPos + ScrollbarTbWidth = scrollInfo.tbWidth + ScrollbarBkgRepCycs = scrollInfo.bkgRep + } + + +let inline updateViewerWidthInWaveSim w (model:Model) = + printfn "updateviewerWidthInWaveSim" // ***> + let wsModel = getWSModel model + //dispatch <| SetViewerWidth w + let namesColWidth = calcNamesColWidth wsModel + + /// The extra is probably because of some unnacounted for padding etc (there is a weird 2px spacer to right of the divider) + /// It also allows space for a scroll bar (about 6 px) + let otherDivWidths = Constants.leftMargin + Constants.rightMargin + DiagramStyle.Constants.dividerBarWidth + Constants.scrollBarWidth + 8 + + /// This is what the overall waveform width must be + let valuesColumnWidth,_ = valuesColumnSize wsModel + let waveColWidth = w - otherDivWidths - namesColWidth - valuesColumnWidth + + /// Require at least one visible clock cycle: otherwise choose number to get close to correct width of 1 cycle + let wholeCycles = + max 1 (int (float waveColWidth / singleWaveWidth wsModel)) + |> min (Constants.maxLastClk / wsModel.CycleMultiplier) // make sure there can be no over-run when making viewer larger + let singleCycleWidth = float waveColWidth / float wholeCycles + let finalWavesColWidth = singleCycleWidth * float wholeCycles + + /// Estimated length of scrollbar, adding three components together: names col, waveform port, and values col. + let scrollbarWidth = (float namesColWidth) + finalWavesColWidth + (float valuesColumnWidth) + + // printfn "DEBUG:updateViewerWidthInWaveSim: Names Column Width = %Apx" (float namesColWidth) + // printfn "DEBUG:updateViewerWidthInWaveSim: Waves Column Width = %Apx" finalWavesColWidth + // printfn "DEBUG:updateViewerWidthInWaveSim: Values Column Width = %Apx" (float valuesColumnWidth) + // printfn "DEBUG:updateViewerWidthInWaveSim: Calculated Scrollbar Width = %Apx" scrollbarWidth + + + let updateFn wsModel = + { + wsModel with + ShownCycles = wholeCycles + StartCycle = min wsModel.StartCycle (Constants.maxLastClk - (wholeCycles - 1)*wsModel.CycleMultiplier) + CurrClkCycle = min wsModel.CurrClkCycle Constants.maxLastClk + WaveformColumnWidth = finalWavesColWidth + ScrollbarBkgWidth = scrollbarWidth + } + |> validateScrollBarInfo + + {model with WaveSimViewerWidth = w} + |> ModelHelpers.updateWSModel updateFn + + + +let inline setViewerWidthInWaveSim w dispatch = + dispatch <| UpdateModel (updateViewerWidthInWaveSim w) + dispatch <| GenerateCurrentWaveforms + +let rec validateSimParas (ws: WaveSimModel) = + if ws.StartCycle < 0 then + printfn $"ERROR in Sim parameters: StartCycle {ws.StartCycle} < 0" + validateSimParas {ws with StartCycle = 0} + elif ws.CurrClkCycleDetail > Constants.maxLastClk then + validateSimParas {ws with CurrClkCycleDetail = Constants.maxLastClk} + elif (ws.StartCycle + ws.ShownCycles-1)*ws.CycleMultiplier > Constants.maxLastClk then + printfn $"Correcting sim paras by reducing StartCycle" + {ws with StartCycle = Constants.maxLastClk / ws.CycleMultiplier - ws.ShownCycles} + elif ws.CurrClkCycle < ws.StartCycle || ws.CurrClkCycle >= ws.StartCycle + ws.ShownCycles then + printfn $"Resetting CurClkCycle which {ws.CurrClkCycle} was too large with multiplier = {ws.CycleMultiplier}" + {ws with CurrClkCycle = ws.StartCycle; CurrClkCycleDetail = ws.StartCycle*ws.CycleMultiplier} + else ws + |> validateScrollBarInfo + +let changeMultiplier newMultiplier (ws: WaveSimModel) = + let oldM = ws.CycleMultiplier + printfn $"Old: {oldM} shown {ws.ShownCycles} start={ws.StartCycle} NewM={newMultiplier}" + let sampsHalf = (float ws.ShownCycles - 1.) / 2. + let newShown = min ws.ShownCycles (Constants.maxLastClk / newMultiplier) + let newStart = int ((float ws.StartCycle + sampsHalf) * float oldM / float newMultiplier - (float newShown - 1.) / 2.) + printfn $"New: shown={newShown} start = {newStart}" + {ws with ShownCycles = newShown; StartCycle = newStart; CycleMultiplier = newMultiplier} + |> validateSimParas + + + + +/// Set highlighted clock cycle number +let setClkCycle (wsModel: WaveSimModel) (dispatch: Msg -> unit) (newRealClkCycle: int) : unit = + let start = TimeHelpers.getTimeMs () + let newDetail = min (max newRealClkCycle 0) Constants.maxLastClk + let mult = wsModel.CycleMultiplier + let newClkCycle = newRealClkCycle / mult + let newClkCycle = min Constants.maxLastClk newClkCycle |> max 0 + + if newClkCycle <= endCycle wsModel then + if newClkCycle < wsModel.StartCycle then + dispatch <| GenerateWaveforms + {wsModel with + StartCycle = newClkCycle + CurrClkCycle = newClkCycle + ClkCycleBoxIsEmpty = false + CurrClkCycleDetail = newDetail + } + else + dispatch <| SetWSModel + {wsModel with + CurrClkCycle = newClkCycle + ClkCycleBoxIsEmpty = false + CurrClkCycleDetail = newDetail + } + else + let newDetail = min newDetail Constants.maxLastClk + let newClkCycle = newDetail / mult + dispatch <| GenerateWaveforms + {wsModel with + StartCycle = newClkCycle - (wsModel.ShownCycles - 1) + CurrClkCycle = newClkCycle + ClkCycleBoxIsEmpty = false + CurrClkCycleDetail = newDetail + } + |> TimeHelpers.instrumentInterval "setClkCycle" start + +/// Move waveform view window by closest integer number of cycles. +/// Current clock cycle (WaveSimModel.CurrClkCycle) is set to beginning or end depending on direction of movement. +/// Update is achieved by dispatching a GenerateWaveforms message. +/// Note the side-effect of clearing the ScrollbarQueueIsEmpty counter. +/// Target WaveSimModel. +/// Dispatch function to send messages with. +/// Number of non-integer cycles to move by. +let setScrollbarTbByCycs (wsm: WaveSimModel) (dispatch: Msg->unit) (moveByCycs: float): unit = + let moveWindowBy = int (System.Math.Round moveByCycs) + let mult = wsm.CycleMultiplier + + /// Return target value when within min and max value, otherwise min or max. + let bound (minV: int) (maxV: int) (tarV: int): int = tarV |> max minV |> min maxV + let minSimCyc = 0 + let maxSimCyc = Constants.maxLastClk / mult + + let newStartCyc = (wsm.StartCycle+moveWindowBy) |> bound minSimCyc (maxSimCyc-wsm.ShownCycles+1) + let newCurrCyc = + let newEndCyc = newStartCyc+wsm.ShownCycles-1 + if newStartCyc <= wsm.CurrClkCycle && wsm.CurrClkCycle <= newEndCyc + then + wsm.CurrClkCycle + else + if abs (wsm.CurrClkCycle - newStartCyc) < abs (wsm.CurrClkCycle - newEndCyc) + then newStartCyc + else newEndCyc + let detail = wsm.CurrClkCycleDetail + let detail = if newCurrCyc <> detail / mult then newCurrCyc * mult else detail + { + wsm with StartCycle = newStartCyc; + CurrClkCycle = newCurrCyc; + CurrClkCycleDetail = detail; + ScrollbarQueueIsEmpty = true + } + |> validateSimParas + |> (fun ws -> dispatch <| GenerateWaveforms ws) + +/// Update WaveSimModel with new ScrollbarTbOffset. +/// Used when starting or clearing scrollbar drag mode. +/// Update is achieved by dispatching a GenerateWaveforms message. +/// Target WaveSimModel. +/// Dispatch function to send messages with. +/// Offset option to be written to WaveSimModel.ScrollbarTbOffset. +let setScrollbarOffset (wsm: WaveSimModel) (dispatch: Msg->unit) (offset: float option): unit = + GenerateWaveforms { wsm with ScrollbarTbOffset = offset; ScrollbarQueueIsEmpty = true } |> dispatch + +/// Update WaveSimModel with new ScrollbarQueueIsEmpty. +/// Used to update is-empty counter to coalesce scrollbar mouse events together. +/// Update is achieved by dispatching a UpdateWSModel message, so as to not clog the queue with GenerateWaveforms messages. +/// Target WaveSimModel. +/// Dispatch function to send messages with. +/// Bool to be written to WaveSimModel.ScrollbarQueueIsEmpty. +let setScrollbarLastX (wsm: WaveSimModel) (dispatch: Msg->unit) (isEmpty: bool): unit = + UpdateWSModel (fun _ -> { wsm with ScrollbarQueueIsEmpty = isEmpty }) |> dispatch + +/// If zoomIn, then increase width of clock cycles (i.e.reduce number of visible cycles). +/// otherwise reduce width. GenerateWaveforms message will reconstitute SVGs after the change. +let changeZoom (wsModel: WaveSimModel) (zoomIn: bool) (dispatch: Msg -> unit) = + let start = TimeHelpers.getTimeMs () + let shownCycles = + let wantedCycles = int (float wsModel.ShownCycles / Constants.zoomChangeFactor) + if zoomIn then + // try to reduce number of cycles displayed + wantedCycles + // If number of cycles after casting to int does not change + |> (fun nc -> if nc = wsModel.ShownCycles then nc - 1 else nc ) + // Require a minimum of cycles + |> (fun nc -> + let minVis = min wsModel.ShownCycles Constants.minVisibleCycles + max nc minVis) + else + let wantedCycles = int (float wsModel.ShownCycles * Constants.zoomChangeFactor) + // try to increase number of cycles displayed + wantedCycles + // If number of cycles after casting to int does not change + |> (fun nc -> if nc = wsModel.ShownCycles then nc + 1 else nc ) + |> (fun nc -> + let maxNc = int (wsModel.WaveformColumnWidth / float Constants.minCycleWidth) + max wsModel.ShownCycles (min nc maxNc)) + let startCycle = + // preferred start cycle to keep centre of screen ok + let sc = (wsModel.StartCycle - (shownCycles - wsModel.ShownCycles)/2) + let cOffset = wsModel.CurrClkCycle - sc + sc + // try to keep cursor on screen + |> (fun sc -> + if cOffset > shownCycles - 1 then + sc + cOffset - shownCycles + 1 + elif cOffset < 0 then + (sc + cOffset) + else + sc) + // final limits check so no cycle is outside allowed range + |> min (Constants.maxLastClk / wsModel.CycleMultiplier - shownCycles) + |> max 0 + + + dispatch <| GenerateWaveforms { + wsModel with + ShownCycles = shownCycles; + StartCycle = startCycle + } + |> TimeHelpers.instrumentInterval "changeZoom" start + +/// Make scrollbar element based on information in WaveSimModel. +/// Called in viewWaveSim, presumably after refreshWaveSim was called. +/// Target WaveSimModel. +/// Dispatch function to send messages with. Not used directly, but passed to tbMouseMoveOp. +/// React element to be placed in to DOM. +let makeScrollbar (wsm: WaveSimModel) (dispatch: Msg->unit): ReactElement = + // button props + let scrollWaveformViewBy (numCycles: float) = setScrollbarTbByCycs wsm dispatch numCycles + + // svg props + let bkgWidth = wsm.ScrollbarBkgWidth - 60. // 60 = 2x width of buttons + + let tbMouseDownHandler (event: Browser.Types.MouseEvent): unit = // start drag + ScrollbarMouseMsg (event.clientX, StartScrollbarDrag, dispatch) |> dispatch + + let tbMouseMoveHandler (event: Browser.Types.MouseEvent): unit = // if in drag, drag; otherwise do nothing + if Option.isSome wsm.ScrollbarTbOffset + then ScrollbarMouseMsg (event.clientX, InScrollbarDrag, dispatch) |> dispatch + else () + + let tbMouseUpHandler (event: Browser.Types.MouseEvent): unit = // if in drag, clear drag; otherwise do nothing + if Option.isSome wsm.ScrollbarTbOffset + then ScrollbarMouseMsg (event.clientX, ClearScrollbarDrag, dispatch) |> dispatch + else () + + let bkgPropList (width: float): List = + [ + HTMLAttr.Id "scrollbarThumb"; + SVGAttr.X $"0px"; SVGAttr.Y "0.5px"; + SVGAttr.Width $"%.1f{width}px"; SVGAttr.Height $"%.1f{WaveSimStyle.Constants.softScrollBarWidth-1.0}px"; + SVGAttr.Fill "white"; SVGAttr.Stroke "gray"; SVGAttr.StrokeWidth "1px"; + ] + + let tbPropList (pos: float) (width: float): List = + [ + HTMLAttr.Id "scrollbarBkg"; + Style [ Cursor "grab"]; + SVGAttr.X $"%.1f{pos}px"; SVGAttr.Y "0.5px"; + SVGAttr.Width $"%.1f{width}px"; SVGAttr.Height $"%.1f{WaveSimStyle.Constants.softScrollBarWidth-1.0}px"; + SVGAttr.Fill "lightgrey"; SVGAttr.Stroke "gray"; SVGAttr.StrokeWidth "1px"; + OnMouseDown tbMouseDownHandler; OnMouseUp tbMouseUpHandler; OnMouseMove tbMouseMoveHandler; + ] + + div [ Style [ MarginTop "5px"; MarginBottom "5px"; Height "25px"]] [ + button [ Button.Props [scrollbarClkCycleLeftStyle] ] + (fun _ -> scrollWaveformViewBy -1.0) + (str "◀") + svg + [Style [Width $"{bkgWidth}"; Height $"{WaveSimStyle.Constants.softScrollBarWidth}px"];] + [ + rect (bkgPropList bkgWidth) []; // background + rect (tbPropList wsm.ScrollbarTbPos wsm.ScrollbarTbWidth) []; // thumb + ] + button [ Button.Props [scrollbarClkCycleRightStyle]] + (fun _ -> scrollWaveformViewBy 1.0) + (str "▶") + ] + +/// Update waveform view information based on mouse postion in the X direction. +/// Called in update when ScrollbarMouseMsg is dispatched. +/// Target WaveSimModel. +/// Dispatch function to send messages with, not used directly. +/// Cursor postion in relation to the screen, i.e. event.clientX. +/// Scrollbar action to do, see choices for more info, in type of ScrollbarMouseAction. +/// Note that screenX does NOT scale with web zoom and will cause weird results! +let updateScrollbar (wsm: WaveSimModel) (dispatch: Msg->unit) (cursor: float) (action: ScrollbarMouseAction): unit = + /// Translate mouse movements in pixels to number of cycles to move by. + /// Linear translator aims to allow scrollbar thumb to follow cursor. + /// Number of pixels mouse has moved in X direction, obtained from MouseMove event. + /// Number of cycles to move by. + /// Swap this out with some other mouse-to-cycle translator for better user experience. + let linearMouseToCycleTranslator (dx: float): float = + let cycleToPixelRatio = float wsm.ScrollbarBkgRepCycs / (wsm.ScrollbarBkgWidth - wsm.ScrollbarTbWidth) + dx*cycleToPixelRatio + match action with + | StartScrollbarDrag -> // record offset + let offset = Some (wsm.ScrollbarTbPos-cursor) + setScrollbarOffset wsm dispatch offset + | InScrollbarDrag -> // in drag, unknown queue state: update counter, if queue is empty then dispatch ReleaseScrollQueue message + let canDispatch = wsm.ScrollbarQueueIsEmpty + setScrollbarLastX wsm dispatch false + if canDispatch then ScrollbarMouseMsg (cursor, ReleaseScrollQueue, dispatch) |> dispatch + | ReleaseScrollQueue -> // in drag, and queue is clear: update and set ScrollbarQueueIsEmpty to true + match wsm.ScrollbarTbOffset with + | Some puckOffset -> + let dx = puckOffset + cursor - wsm.ScrollbarTbPos // offset + new cursor = new thumb; dx = new thumb - old thumb + setScrollbarTbByCycs wsm dispatch (linearMouseToCycleTranslator dx) + | None -> () + | ClearScrollbarDrag -> // clear offset + setScrollbarOffset wsm dispatch None + + /// Click on these buttons to change the number of visible clock cycles. +let zoomButtons (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElement = + div [ clkCycleButtonStyle ] + [ + button [ Button.Props [clkCycleLeftStyle] ] + (fun _ -> changeZoom wsModel false dispatch) + zoomOutSVG + button [ Button.Props [clkCycleRightStyle] ] + (fun _ -> changeZoom wsModel true dispatch) + zoomInSVG + ] + +/// Click on these to change the highlighted clock cycle. +let clkCycleButtons (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElement = + /// Controls the number of cycles moved by the "◀◀" and "▶▶" buttons + let mult = wsModel.CycleMultiplier + let bigStepSize = max (2*mult) (wsModel.ShownCycles*mult / 2) + + let scrollWaveformsBy (numCycles: int) = + setClkCycle wsModel dispatch (wsModel.CurrClkCycleDetail + numCycles) + + div [ clkCycleButtonStyle ] + [ + // Move left by bigStepSize cycles + button [ Button.Props [clkCycleLeftStyle] ] + (fun _ -> scrollWaveformsBy -bigStepSize) + (str "◀◀") + + // Move left by one cycle + button [ Button.Props [clkCycleInnerStyle] ] + (fun _ -> scrollWaveformsBy -1) + (str "◀") + + // Text input box for manual selection of clock cycle + Input.number [ + Input.Props clkCycleInputProps + + Input.Value ( + match wsModel.ClkCycleBoxIsEmpty with + | true -> "" + | false -> string (wsModel.CurrClkCycleDetail) + ) + // TODO: Test more properly with invalid inputs (including negative numbers) + Input.OnChange(fun c -> + match System.Int32.TryParse c.Value with + | true, n -> + setClkCycle wsModel dispatch n + | false, _ when c.Value = "" -> + dispatch <| SetWSModel {wsModel with ClkCycleBoxIsEmpty = true} + | _ -> + dispatch <| SetWSModel {wsModel with ClkCycleBoxIsEmpty = false} + ) + ] + + // Move right by one cycle + button [ Button.Props [clkCycleInnerStyle] ] + (fun _ -> scrollWaveformsBy 1) + (str "▶") + + // Move right by bigStepSize cycles + button [ Button.Props [clkCycleRightStyle] ] + (fun _ -> scrollWaveformsBy bigStepSize) + (str "▶▶") + ] + +//-------------------------------------Popup menu for multiplier---------------------------------------------// + +let multiplierMenuButton(wsModel: WaveSimModel) (dispatch: Msg -> unit) = + /// key = 0 .. n-1 where there are n possible multipliers + let mulTable = Constants.multipliers + let menuItem (key) = + let itemLegend = str (match key with | 0 -> "Every Cycle" | _ -> $"Every {mulTable[key]} cycles") + Menu.Item.li + [ Menu.Item.IsActive (Constants.multipliers[key] = wsModel.CycleMultiplier) + Menu.Item.OnClick (fun _ -> + dispatch <| ChangeWaveSimMultiplier (key); + dispatch ClosePopup) + ] + [ itemLegend ] + let menu = + div [] + [ + h5 [Style[Color IColor.IsDanger]] [str "Warning: extra zoom view greater than X1 will sample the waveform and so lose information about fast-changing outputs"] + br [] + h5 [] [str "It should only be used to observe slow-chnaging signals, when it is necessary to see large clock cycle range, \ + and the normal - zoom function is not enough."] + br [] + Menu.menu [] [ Menu.list [] (List.map menuItem [0 .. mulTable.Length - 1 ])] + ] + + let buttonClick = Button.OnClick (fun _ -> + printfn $"Mul={wsModel.CycleMultiplier}" + dispatch <| ShowStaticInfoPopup("Multiplier",menu,dispatch )) + Button.button ( buttonClick :: topHalfButtonProps IColor.IsDanger "ZoomButton" false) [str $"Extra zoom X{wsModel.CycleMultiplier}"] diff --git a/src/Renderer/UI/WaveSim/WaveSimRams.fs b/src/Renderer/UI/WaveSim/WaveSimRams.fs new file mode 100644 index 000000000..48d4b23f8 --- /dev/null +++ b/src/Renderer/UI/WaveSim/WaveSimRams.fs @@ -0,0 +1,216 @@ +module WaveSimRams + +open Fulma +open Fable.React +open Fable.React.Props + +open CommonTypes +open ModelType +open ModelHelpers +open WaveSimStyle +open WaveSimHelpers +open TopMenuView +open SimulatorTypes +open NumberHelpers +open DrawModelType +open WaveSimNavigation +open WaveSimSelect +open DiagramStyle + + +/// Table row that shows the address and data of a RAM component. +let ramTableRow ((addr, data,rowType): string * string * RamRowType): ReactElement = + + tr [ Style <| ramTableRowStyle rowType ] [ + td [] [ str addr ] + td [] [ str data ] + ] + +/// Table showing contents of a RAM component. +let ramTable (wsModel: WaveSimModel) ((ramId, ramLabel): FComponentId * string) : ReactElement = + let wanted = calcWaveformAndScrollBarHeight wsModel + let maxHeight = max (screenHeight() - (min wanted (screenHeight()/2.)) - 300.) 30. + let fs = wsModel.FastSim + match Map.tryFind ramId wsModel.FastSim.FComps with + | None -> div [] [] + | Some fc -> + let step = wsModel.CurrClkCycle + FastRun.runFastSimulation None step fs |> ignore // not sure why this is needed + + // in some cases fast sim is run for one cycle less than currClockCycle + let memData = + match fc.FType with + | ROM1 mem + | AsyncROM1 mem -> mem + | RAM1 mem + | AsyncRAM1 mem -> + match FastRun.extractFastSimulationState fs wsModel.CurrClkCycle ramId with + |RamState mem -> mem + | x -> failwithf $"What? Unexpected state {x} from cycle {wsModel.CurrClkCycle} \ + in RAM component '{ramLabel}'. FastSim step = {fs.ClockTick}" + | _ -> failwithf $"Given a component {fc.FType} which is not a vaild RAM" + let aWidth,dWidth = memData.AddressWidth,memData.WordWidth + + let print w (a:int64) = NumberHelpers.valToPaddedString w wsModel.Radix (((1L <<< w) - 1L) &&& a) + + let lastLocation = int64 ((2 <<< memData.AddressWidth - 1) - 1) + + /// print a single 0 location as one table row + let print1 (a:int64,b:int64,rw:RamRowType) = $"{print aWidth a}",$"{print dWidth b}",rw + /// print a range of zero locations as one table row + + let print2 (a1:int64) (a2:int64) (d:int64) = $"{print aWidth (a1+1L)} ... {print aWidth (a2-1L)}", $"{print dWidth d}",RAMNormal + + /// output info for one table row filling the given zero memory gap or arbitrary size, or no line if there is no gap. + let printGap (gStart:int64) (gEnd:int64) = + match gEnd - gStart with + | 1L -> [] + | 2L -> [print1 ((gEnd + gStart) / 2L, 0L,RAMNormal)] + | n when n > 2L -> + [print2 gStart gEnd 0L] + | _ -> + failwithf $"What? gEnd={gEnd},gStart={gStart}: negative or zero gaps are impossible..." + + /// transform Sparse RAM info into strings to print in a table, adding extra lines for zero gaps + /// line styling is controlled by a RamRowtype value and added later when the table row react is generated + let addGapLines (items: (int64*int64*RamRowType) list) = + let startItem = + match items[0] with + | -1L,_,_ -> [] + | gStart,dStart,rw-> [print1 (gStart,dStart,rw)] + List.pairwise items + |> List.collect (fun ((gStart,_,_),(gEnd,dEnd,rwe)) -> + let thisItem = if gEnd = lastLocation + 1L then [] else [print1 (gEnd,dEnd,rwe)] + [printGap gStart gEnd; thisItem]) + |> List.concat + + /// Add a RAMNormal RamRowType value to every location in mem. + /// Add in additional locations for read and/or write if needed. + /// Set RamRowValue type to RAMWritten or RAMRead for thse locations. + /// Write is always 1 cycle after WEN=1 and address. + /// Read is 1 (0) cycles after address for sync (asynch) memories. + let addReadWrite (fc:FastComponent) (step:int) (mem: Map) = + let getInt64 (a: IOArray) step = + let w = a.Width + match w with + | w when w > 32 -> int64 <| convertBigIntToUInt64 w a.BigIntStep[step] + | _ -> int64 <| a.UInt32Step[step] + + let readStep = + match fc.FType with + | AsyncROM1 _ | AsyncRAM1 _ -> step + | ROM1 _ | RAM1 _ -> step - 1 + | _ -> failwithf $"What? {fc.FullName} should be a memory component" + + let addrSteps step = getInt64 fc.InputLinks[0] step + + let readOpt = + match step, fc.FType with + | 0,ROM1 _ | 0, RAM1 _ -> None + | _ -> + addrSteps readStep + |> Some + let writeOpt = + match step, fc.FType with + | _, ROM1 _ + | _, AsyncROM1 _ + | 0, _ -> None + | _, RAM1 _ | _, AsyncRAM1 _ when getInt64 fc.InputLinks[2] (step-1) = 1L -> + addrSteps (step-1) + |> Some + | _ -> + None + + /// Mark addr in memory map as being rType + /// if addr does not exist - create it + let addToMap rType addr mem:Map = + match Map.tryFind addr mem with + | Some (d,_) -> Map.add addr (d,rType) mem + | None -> Map.add addr (0L,rType) mem + + + Map.map (fun k v -> v,RAMNormal) mem + |> (fun mem -> + match readOpt with + | Some addr -> addToMap RAMRead addr mem + | None -> mem + |> (fun mem -> + match writeOpt with // overwrite RAMRead here is need be + | Some addr -> addToMap RAMWritten addr mem + | None -> mem)) + + + /// add fake locations beyong normal address range so that + /// addGapLines fills these (if need be). These locations are then removed + let addEndPoints (items:(int64*int64*RamRowType) list) = + let ad (a,d,rw) = a + match items.Length with + | 0 -> [-1L,0L,RAMNormal; lastLocation,0L,RAMNormal] + | _ -> + if ad items[0] < 0L then items else List.insertAt 0 (-1L,-1L,RAMNormal) items + |> (fun items -> + if ad items[items.Length-1] = lastLocation then + items else + List.insertAt items.Length (lastLocation+1L,0L,RAMNormal) items) + + + let lineItems = + memData.Data + |> addReadWrite fc step + |> Map.toList + |> List.map (fun (a,(d,rw)) -> a,d,rw) + |> List.filter (fun (a,d,rw) -> d<>0L || rw <> RAMNormal) + |> List.sort + |> addEndPoints + |> addGapLines + + + + Level.item [ + Level.Item.Option.Props ramTableLevelProps + Level.Item.Option.HasTextCentered + ] [ + Heading.h6 [ + Heading.Option.Props [ centerAlignStyle ] + ] [ str ramLabel ] + div [Style [MaxHeight maxHeight;OverflowY OverflowOptions.Auto]] [ + Table.table [ + Table.IsFullWidth + Table.IsBordered + ] [ thead [] [ + tr [] [ + th [ centerAlignStyle ] [ str "Address"] + th [ centerAlignStyle ] [ str "Data"; sub [Style [MarginLeft "2px"; FontSize "10px"]] [str (string wsModel.CurrClkCycle)]] + ] + ] + tbody [] + (List.map ramTableRow lineItems) + ] ] + br [] + ] + +/// Bulma Level component of tables showing RAM contents. +let ramTables (wsModel: WaveSimModel) : ReactElement = + let inlineStyle (styles:CSSProp list) = div [Style (Display DisplayOptions.Inline :: styles)] + let start = TimeHelpers.getTimeMs () + let selectedRams = Map.toList wsModel.SelectedRams + if List.length selectedRams > 0 then + let tables = + let headerRow = + ["read", RAMRead; "overwritten",RAMWritten] + |> List.map (fun (op, opStyle) -> inlineStyle [Margin "0px"] [inlineStyle (ramTableRowStyle opStyle) [str op]]) + |> function + | [a;b] -> [str "Key: Memory location is " ; a; str ", or " ;b; str ". Click waveforms or use cursor control to change current cycle."] + | _ -> failwithf "What? Can't happen!" + List.map (fun ram -> td [Style [BorderColor "white"]] [ramTable wsModel ram]) selectedRams + |> (fun tables -> [tbody [] [tr [] [th [ColSpan selectedRams.Length] [inlineStyle [] headerRow]]; tr [Style [Border "10px"]] tables]]) + |> Fulma.Table.table [ + Table.TableOption.Props ramTablesLevelProps; + Table.IsFullWidth; + Table.IsBordered; + ] + div [HTMLAttr.Id "TablesDiv"] [ hr [ Style [ Margin "5px"]]; br [ Style [ Margin "0px"]]; tables] + else div [] [] + |> TimeHelpers.instrumentInterval "ramTables" start + + diff --git a/src/Renderer/UI/WaveSim/WaveSimSelect.fs b/src/Renderer/UI/WaveSim/WaveSimSelect.fs index 09665be25..55e01d995 100644 --- a/src/Renderer/UI/WaveSim/WaveSimSelect.fs +++ b/src/Renderer/UI/WaveSim/WaveSimSelect.fs @@ -300,20 +300,6 @@ let makeWave (ws: WaveSimModel) (fastSim: FastSimulation) (wi: WaveIndexT) : Wav -/// Get all simulatable waves from CanvasState. Includes top-level Input and Output ports. -/// Waves contain info which will be used later to create the SVGs for those waves actually -/// selected. Init value of these from this function is None. -let getWaves (ws: WaveSimModel) (fs: FastSimulation) : Map = - let start = TimeHelpers.getTimeMs () - //printfn $"{fs.WaveIndex.Length} possible waves" - fs.WaveIndex - |> TimeHelpers.instrumentInterval "getAllPorts" start - |> Array.map (fun wi -> wi, makeWave ws fs wi) - //|> fun x -> printfn $"Made waves!";x - |> Map.ofArray - |> TimeHelpers.instrumentInterval "makeWavePipeline" start - - /// Sets all waves as selected or not selected depending on value of selected @@ -775,24 +761,4 @@ let selectRamModal (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElemen ] -//-------------------------------------Popup menu for multiplier---------------------------------------------// - -let multiplierMenuButton(wsModel: WaveSimModel) (dispatch: Msg -> unit) = - /// key = 0 .. n-1 where there are n possible multipliers - let mulTable = Constants.multipliers - let menuItem (key) = - let itemLegend = str (match key with | 0 -> "Every Cycle" | _ -> $"Every {mulTable[key]} cycles") - Menu.Item.li - [ Menu.Item.IsActive (Constants.multipliers[key] = wsModel.CycleMultiplier) - Menu.Item.OnClick (fun _ -> - dispatch <| ChangeWaveSimMultiplier (key); - dispatch ClosePopup) - ] - [ itemLegend ] - let menu = Menu.menu [] [ Menu.list [] (List.map menuItem [0 .. mulTable.Length - 1 ]) ] - - let buttonClick = Button.OnClick (fun _ -> - printfn $"Mul={wsModel.CycleMultiplier}" - dispatch <| ShowStaticInfoPopup("Multiplier",menu,dispatch )) - Button.button ( buttonClick :: topHalfButtonProps IColor.IsDanger "ZoomButton" false) [str $"zoom X{wsModel.CycleMultiplier}"] diff --git a/src/Renderer/UI/WaveSim/WaveSimStyle.fs b/src/Renderer/UI/WaveSim/WaveSimStyle.fs index a92547c80..4efa62526 100644 --- a/src/Renderer/UI/WaveSim/WaveSimStyle.fs +++ b/src/Renderer/UI/WaveSim/WaveSimStyle.fs @@ -1,12 +1,14 @@ module WaveSimStyle -open ModelType -open ModelHelpers -open WaveSimHelpers + + open Fulma open Fable.React open Fable.React.Props +open CommonTypes +open ModelType +open ModelHelpers module Constants = // Width of names column - replaced by calcNamesColWidth function @@ -67,8 +69,92 @@ module Constants = /// height of the top half of the wave sim window (including tabs) when waveforms are displayed let topHalfHeight = 260. + // helpers constants + /// initial time running simulation without spinner to check speed (in ms) + let initSimulationTime = 100. + /// max estimated time to run simulation and not need a spinner (in ms) + let maxSimulationTimeWithoutSpinner = 200. + /// The horizontal length of a transition cross-hatch for non-binary waveforms + let nonBinaryTransLen : float = 2. + + /// The height of the viewbox used for a wave's SVG. This is the same as the height + /// of a label in the name and value columns. + /// TODO: Combine this with WaveSimStyle.Constants.rowHeight? + let viewBoxHeight : float = 30.0 + + /// Height of a waveform + let waveHeight : float = 0.8 * viewBoxHeight + /// Vertical padding between top and bottom of each wave and the row it is in. + let spacing : float = (viewBoxHeight - waveHeight) / 2. + + /// y-coordinate of the top of a waveform + let yTop = spacing + /// y-coordiante of the bottom of a waveform + let yBot = waveHeight + spacing + + /// minium number of cycles on screen when zooming in + let minVisibleCycles = 3 + + /// Minimum number of visible clock cycles. + let minCycleWidth = 5 + + let zoomChangeFactor = 1.5 + + /// If the width of a non-binary waveform is less than this value, display a cross-hatch + /// to indicate a non-binary wave is rapidly changing value. + let clkCycleNarrowThreshold = 20 + + /// number of extra steps simulated beyond that used in simulation. Is this needed? + let extraSimulatedSteps = 5 + + let infoMessage = + "Find ports by any part of their name. '.' = show all. '*' = show selected. '-' = collapse all" + + let outOfDateMessage = "Use refresh button to update waveforms. 'End' and then 'Start' to simulate a different sheet" + + let infoSignUnicode = "\U0001F6C8" + + let waveLegendMaxChars = 35 + let valueColumnMaxChars = 35 + let multipliers = [1;2;5;10;20;50] + +// maybe these should be defined earlier in compile order? Or added as list functions? + +let listMaxWithDef defaultValue lst = + defaultValue :: lst + |> List.max + +let listCollectSomes mapFn lst = + lst + |> List.collect (fun x -> match mapFn x with | Some r -> [r] | None -> []) + +/// List of selected waves (of type Wave) +let selectedWaves (wsModel: WaveSimModel) : Wave list = + wsModel.SelectedWaves + |> List.map (fun wi -> Map.tryFind wi wsModel.AllWaves |> Option.toList) + |> List.concat + +/// Convert XYPos list to string +let pointsToString (points: XYPos array) : string = + Array.fold (fun str (point: XYPos) -> + $"{str} %.1f{point.X},%.1f{point.Y} " + ) "" points + let screenHeight() = Browser.Dom.document.defaultView.innerHeight + +/// Width of one clock cycle. +let singleWaveWidth m = max 5.0 (float m.WaveformColumnWidth / float m.ShownCycles) + +/// Left-most coordinate of the SVG viewbox. +let viewBoxMinX m = string (float m.StartCycle * singleWaveWidth m) + +/// Total width of the SVG viewbox. +let viewBoxWidth m = string (max 5.0 (m.WaveformColumnWidth)) + +/// Right-most visible clock cycle. +let endCycle wsModel = wsModel.StartCycle + (wsModel.ShownCycles) - 1 + /// Style for top row in wave viewer. let topRowStyle = Style [ Height Constants.rowHeight @@ -417,7 +503,7 @@ let valueLabelStyle = let nameRowLevelLeftProps (visibility: string): IHTMLProp list = [ Style [ Position PositionOptions.Sticky - Left 0 + CSSProp.Left 0 Visibility visibility ] ] @@ -712,6 +798,54 @@ let wavePolyfillStyle points : IProp list = [ Points (pointsToString points) ] +/// Style for top half of waveform simulator (instructions and buttons) +let topHalfStyle = Style [ + Position PositionOptions.Sticky + CSSProp.Top 0 + BackgroundColor "white" + ZIndex 10000 +] + +//---------------------------Code for selector details state----------------------------------// + +// It would be better to do this with one subfunction and Optics! + +/// Sets or clears a subset of ShowSheetDetail +let setWaveSheetSelectionOpen (wsModel: WaveSimModel) (subSheets: string list list) (show: bool) = + let setChange = Set.ofList subSheets + let newSelect = + match show with + | false -> Set.difference wsModel.ShowSheetDetail setChange + | true -> Set.union setChange wsModel.ShowSheetDetail + {wsModel with ShowSheetDetail = newSelect} + +/// Sets or clears a subset of ShowComponentDetail +let setWaveComponentSelectionOpen (wsModel: WaveSimModel) (fIds: FComponentId list) (show: bool) = + let fIdSet = Set.ofList fIds + let newSelect = + match show with + | true -> Set.union fIdSet wsModel.ShowComponentDetail + | false -> Set.difference wsModel.ShowComponentDetail fIdSet + {wsModel with ShowComponentDetail = newSelect} + + +/// Sets or clears a subset of ShowGroupDetail +let setWaveGroupSelectionOpen (wsModel: WaveSimModel) (grps :(ComponentGroup*string list) list) (show: bool) = + let grpSet = Set.ofList grps + let newSelect = + match show with + | true -> Set.union grpSet wsModel.ShowGroupDetail + | false -> Set.difference wsModel.ShowGroupDetail grpSet + {wsModel with ShowGroupDetail = newSelect} + +let setSelectionOpen (wsModel: WaveSimModel) (cBox: CheckBoxStyle) (show:bool) = + match cBox with + | PortItem _ -> failwithf "What? setselectionopen cannot be called from a Port" + | ComponentItem fc -> setWaveComponentSelectionOpen wsModel [fc.fId] show + | GroupItem (grp,subSheet) -> setWaveGroupSelectionOpen wsModel [grp,subSheet] show + | SheetItem subSheet -> setWaveSheetSelectionOpen wsModel [subSheet] show + + /// Props for HTML Summary element let summaryProps (isSummary:bool) cBox (ws: WaveSimModel) (dispatch: Msg -> Unit): IHTMLProp list = [ @@ -749,61 +883,6 @@ let detailsProps showDetails cBox (ws: WaveSimModel) (dispatch: Msg -> Unit): IH Open (show || showDetails) ] -/// Style for top half of waveform simulator (instructions and buttons) -let topHalfStyle = Style [ - Position PositionOptions.Sticky - Top 0 - BackgroundColor "white" - ZIndex 10000 -] - - - - - - -let inline updateViewerWidthInWaveSim w (model:Model) = - printfn "updateviewerWidthInWaveSim" // ***> - let wsModel = getWSModel model - //dispatch <| SetViewerWidth w - let namesColWidth = calcNamesColWidth wsModel - - /// The extra is probably because of some unnacounted for padding etc (there is a weird 2px spacer to right of the divider) - /// It also allows space for a scroll bar (about 6 px) - let otherDivWidths = Constants.leftMargin + Constants.rightMargin + DiagramStyle.Constants.dividerBarWidth + Constants.scrollBarWidth + 8 - - /// This is what the overall waveform width must be - let valuesColumnWidth,_ = valuesColumnSize wsModel - let waveColWidth = w - otherDivWidths - namesColWidth - valuesColumnWidth - - /// Require at least one visible clock cycle: otherwise choose number to get close to correct width of 1 cycle - let wholeCycles = max 1 (int (float waveColWidth / singleWaveWidth wsModel)) - let singleCycleWidth = float waveColWidth / float wholeCycles - let finalWavesColWidth = singleCycleWidth * float wholeCycles - - /// Estimated length of scrollbar, adding three components together: names col, waveform port, and values col. - let scrollbarWidth = (float namesColWidth) + finalWavesColWidth + (float valuesColumnWidth) - - // printfn "DEBUG:updateViewerWidthInWaveSim: Names Column Width = %Apx" (float namesColWidth) - // printfn "DEBUG:updateViewerWidthInWaveSim: Waves Column Width = %Apx" finalWavesColWidth - // printfn "DEBUG:updateViewerWidthInWaveSim: Values Column Width = %Apx" (float valuesColumnWidth) - // printfn "DEBUG:updateViewerWidthInWaveSim: Calculated Scrollbar Width = %Apx" scrollbarWidth - - let updateFn wsModel = - { - wsModel with - ShownCycles = wholeCycles - StartCycle = min wsModel.StartCycle (Constants.maxLastClk - wholeCycles + 1) - CurrClkCycle = min wsModel.CurrClkCycle Constants.maxLastClk - WaveformColumnWidth = finalWavesColWidth - ScrollbarBkgWidth = scrollbarWidth - } - - {model with WaveSimViewerWidth = w} - |> ModelHelpers.updateWSModel updateFn -let inline setViewerWidthInWaveSim w dispatch = - dispatch <| UpdateModel (updateViewerWidthInWaveSim w) - dispatch <| GenerateCurrentWaveforms diff --git a/src/Renderer/UI/WaveSim/WaveSimWaveforms.fs b/src/Renderer/UI/WaveSim/WaveSimWaveforms.fs new file mode 100644 index 000000000..8a9b1e867 --- /dev/null +++ b/src/Renderer/UI/WaveSim/WaveSimWaveforms.fs @@ -0,0 +1,280 @@ +module WaveSimWaveforms + +open Fulma +open Fable.React +open Fable.React.Props + +open CommonTypes +open ModelType +open ModelHelpers +open WaveSimStyle +open WaveSimHelpers +open TopMenuView +open SimulatorTypes +open NumberHelpers +open DrawModelType +open WaveSimNavigation +open WaveSimSelect +open DiagramStyle + +/// ReactElement of the tabs for changing displayed radix +let radixButtons (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElement = + let radixString = [ + Bin, "Bin" + Hex, "Hex" + Dec, "uDec" + SDec, "sDec" + ] + + let radixTab (radix, radixStr) = + Tabs.tab [ + Tabs.Tab.IsActive(wsModel.Radix = radix) + Tabs.Tab.Props radixTabProps + ] [ a [ + radixTabAStyle + OnClick(fun _ -> dispatch <| GenerateWaveforms {wsModel with Radix = radix}) + ] [ str radixStr ] + ] + + Tabs.tabs [ + Tabs.IsToggle + Tabs.Props [ radixTabsStyle ] + ] (List.map (radixTab) radixString) + + +let highlightCircuit fs comps wave (dispatch: Msg -> Unit) = + dispatch <| Sheet (SheetT.Msg.Wire (BusWireT.Msg.Symbol (SymbolT.SelectSymbols comps))) + // Filter out any non-existent wires + let conns = connsOfWave fs wave + dispatch <| Sheet (SheetT.Msg.SelectWires conns) + +/// Create label of waveform name for each selected wave. +/// Note that this is generated after calling selectedWaves. Any changes to this function +/// must also be made to valueRows and waveRows, as the order of the waves matters here. +/// This is because the wave viewer is comprised of three columns of many rows, rather +/// than many rows of three columns. +let nameRows (model: Model) (wsModel: WaveSimModel) dispatch: ReactElement list = + selectedWaves wsModel + |> List.map (fun wave -> + let visibility = + if wsModel.HoveredLabel = Some wave.WaveId then + "visible" + else "hidden" + + Level.level [ + Level.Level.Option.Props [ + nameRowLevelStyle (wsModel.HoveredLabel = Some wave.WaveId) + let execWithModel (f: Model -> Unit) = ExecFuncInMessage((fun model _ -> f model), dispatch) + OnMouseOver (fun _ -> dispatch <| execWithModel (fun model -> + if wsModel.DraggedIndex = None then + dispatch <| SetWSModel {wsModel with HoveredLabel = Some wave.WaveId} + // Check if symbol exists on Canvas + let symbols = model.Sheet.Wire.Symbol.Symbols + match Map.tryFind (fst wave.WaveId.Id) symbols with + | Some {Component={Type=IOLabel;Label=lab}} -> + let labelComps = + symbols + |> Map.toList + |> List.map (fun (_,sym) -> sym.Component) + |> List.filter (function | {Type=IOLabel;Label = lab'} when lab' = lab -> true |_ -> false) + |> List.map (fun comp -> ComponentId comp.Id) + highlightCircuit wsModel.FastSim labelComps wave dispatch + | Some sym -> + highlightCircuit wsModel.FastSim [fst wave.WaveId.Id] wave dispatch + | None -> ()) + + ) + OnMouseOut (fun _ -> + dispatch <| SetWSModel {wsModel with HoveredLabel = None} + dispatch <| Sheet (SheetT.Msg.Wire (BusWireT.Msg.Symbol (SymbolT.SelectSymbols []))) + dispatch <| Sheet (SheetT.Msg.UpdateSelectedWires (connsOfWave wsModel.FastSim wave, false)) + ) + + Draggable true + + OnDragStart (fun ev -> + ev.dataTransfer.effectAllowed <- "move" + ev.dataTransfer.dropEffect <- "move" + dispatch <| SetWSModel { + wsModel with + DraggedIndex = Some wave.WaveId + PrevSelectedWaves = Some wsModel.SelectedWaves + } + ) + + OnDrag (fun ev -> + ev.dataTransfer.dropEffect <- "move" + let nameColEl = Browser.Dom.document.getElementById "namesColumn" + let bcr = nameColEl.getBoundingClientRect () + + // If the user drags the label outside the bounds of the wave name column + if ev.clientX < bcr.left || ev.clientX > bcr.right || + ev.clientY < bcr.top || ev.clientY > bcr.bottom + then + dispatch <| SetWSModel { + wsModel with + HoveredLabel = Some wave.WaveId + // Use wsModel.SelectedValues if somehow PrevSelectedWaves not set + SelectedWaves = Option.defaultValue wsModel.SelectedWaves wsModel.PrevSelectedWaves + } + ) + + OnDragOver (fun ev -> ev.preventDefault ()) + + OnDragEnter (fun ev -> + ev.preventDefault () + ev.dataTransfer.dropEffect <- "move" + let nameColEl = Browser.Dom.document.getElementById "namesColumn" + let bcr = nameColEl.getBoundingClientRect () + let index = int (ev.clientY - bcr.top) / Constants.rowHeight - 1 + let draggedWave = + match wsModel.DraggedIndex with + | Some waveId -> [waveId] + | None -> [] + + let selectedWaves = + wsModel.SelectedWaves + |> List.except draggedWave + |> List.insertManyAt index draggedWave + + dispatch <| SetWSModel {wsModel with SelectedWaves = selectedWaves} + ) + + OnDragEnd (fun _ -> + dispatch <| SetWSModel { + wsModel with + DraggedIndex = None + PrevSelectedWaves = None + } + ) + ] + ] [ Level.left + [ Props (nameRowLevelLeftProps visibility) ] + [ Delete.delete [ + Delete.Option.Size IsSmall + Delete.Option.Props [ + OnClick (fun _ -> + let selectedWaves = List.except [wave.WaveId] wsModel.SelectedWaves + dispatch <| SetWSModel {wsModel with SelectedWaves = selectedWaves} + ) + ] + ] [] + ] + Level.right + [ Props [ Style [ PaddingRight Constants.labelPadding ] ] ] + [ label [ nameLabelStyle (wsModel.HoveredLabel = Some wave.WaveId) ] [ wave.ViewerDisplayName|> str ] ] + ] + ) + +/// Create column of waveform names +let namesColumn model wsModel dispatch : ReactElement = + let start = TimeHelpers.getTimeMs () + let rows = + nameRows model wsModel dispatch + div (namesColumnProps wsModel) + (List.concat [ topRow []; rows ]) + |> TimeHelpers.instrumentInterval "namesColumn" start + + +/// Create label of waveform value for each selected wave at a given clk cycle. +/// Note that this is generated after calling selectedWaves. +/// Any changes to this function must also be made to nameRows +/// and waveRows, as the order of the waves matters here. This is +/// because the wave viewer is comprised of three columns of many +/// rows, rather than many rows of three columns. +/// Return required width of values column in pixels, and list of cloumn react elements. +let valueRows (wsModel: WaveSimModel) = + let valueColWidth, valueColNumChars = + valuesColumnSize wsModel + selectedWaves wsModel + |> List.map (fun wave -> getWaveValue wsModel.CurrClkCycleDetail wave wave.Width) + |> List.map (fun fd -> + match fd.Width, fd.Dat with + | 1, Word b -> $" {b}" + | _ -> fastDataToPaddedString valueColNumChars wsModel.Radix fd) + |> List.map (fun value -> label [ valueLabelStyle ] [ str value ]) + |> (fun rows -> valueColWidth, rows) + + +/// Generate a row of numbers in the waveforms column. +/// Numbers correspond to clock cycles multiplied by the current multiplier +let clkCycleNumberRow (wsModel: WaveSimModel) = + let makeClkCycleLabel i = + let n = i * wsModel.CycleMultiplier + match singleWaveWidth wsModel with + | width when width < float Constants.clkCycleNarrowThreshold && i % 5 <> 0 -> + [] + | width when n >= 1000 && width < (float Constants.clkCycleNarrowThreshold * 4. / 3.) && i % 10 <> 0 -> + [] + | _ -> + [ text (clkCycleText wsModel i) [str (string n)] ] + + + [ wsModel.StartCycle .. endCycle wsModel] + |> List.collect makeClkCycleLabel + |> svg (clkCycleNumberRowProps wsModel) + +/// Create column of waveform values +let private valuesColumn wsModel : ReactElement = + let start = TimeHelpers.getTimeMs () + let width, rows = valueRows wsModel + let cursorClkNum = wsModel.CurrClkCycleDetail + let topRowNumber = [ text [Style [FontWeight "bold"; PaddingLeft "2pt"]] [str (string <| cursorClkNum)] ] + + div [ HTMLAttr.Id "ValuesCol" ; valuesColumnStyle width] + (List.concat [ topRow topRowNumber ; rows ]) + |> TimeHelpers.instrumentInterval "valuesColumn" start + +/// Generate a column of waveforms corresponding to selected waves. +let waveformColumn (wsModel: WaveSimModel) dispatch : ReactElement = + let start = TimeHelpers.getTimeMs () + /// Note that this is generated after calling selectedWaves. + /// Any changes to this function must also be made to nameRows + /// and valueRows, as the order of the waves matters here. This is + /// because the wave viewer is comprised of three columns of many + /// rows, rather than many rows of three columns. + let waves = selectedWaves wsModel + if List.exists (fun wave -> wave.SVG = None) waves then + dispatch <| GenerateCurrentWaveforms + let waveRows : ReactElement list = + waves + |> List.map (fun wave -> + match wave.SVG with + | Some waveform -> + waveform + | None -> + div [] [] // the GenerateCurrentWaveforms message will soon update this + ) + + div [ waveformColumnStyle ] + [ + clkCycleHighlightSVG wsModel dispatch + div [ waveRowsStyle <| wsModel.WaveformColumnWidth] + ([ clkCycleNumberRow wsModel ] @ + waveRows + ) + ] + |> TimeHelpers.instrumentInterval "waveformColumn" start + +/// Display the names, waveforms, and values of selected waveforms +let showWaveforms (model: Model) (wsModel: WaveSimModel) (dispatch: Msg -> unit) : ReactElement = + if List.isEmpty wsModel.SelectedWaves then + div [] [] // no waveforms + else + let wHeight = calcWaveformHeight wsModel + let fixedHeight = Constants.softScrollBarWidth + Constants.topHalfHeight + let cssHeight = + if wsModel.SelectedRams.Count > 0 then + $"min( calc(50vh - (0.5 * {fixedHeight}px)) , {wHeight}px)" + else + $"min( calc(100vh - {fixedHeight}px) , {wHeight}px)" + + div [ HTMLAttr.Id "Scroller"; Style [ Height cssHeight; Width "100%"; CSSProp.Custom("overflow", "auto")]] [ + div [ HTMLAttr.Id "WaveCols" ;showWaveformsStyle ] + [ + namesColumn model wsModel dispatch + waveformColumn wsModel dispatch + valuesColumn wsModel + ] + ] + diff --git a/src/Renderer/UI/WaveSim/WaveSimWaves.fs b/src/Renderer/UI/WaveSim/WaveSimWaves.fs new file mode 100644 index 000000000..77b2aeff9 --- /dev/null +++ b/src/Renderer/UI/WaveSim/WaveSimWaves.fs @@ -0,0 +1,329 @@ +module WaveSimWaves + +open Fulma +open Fable.React +open Fable.React.Props + +open CommonTypes +open ModelType +open ModelHelpers +open WaveSimStyle +open WaveSimHelpers +open TopMenuView +open SimulatorTypes +open NumberHelpers +open DrawModelType +open WaveSimNavigation +open WaveSimSelect +open DiagramStyle + + +module Constants = + /// Config variable to choose whether to generate the full 1000 cycles of SVG. + let generateVisibleOnly = true + /// Config variable to choose whether to print performance analysis info to console. + let showPerfLogs = false + let inlineNoWrap = WhiteSpace WhiteSpaceOptions.Nowrap + +open Constants + +/// Get all simulatable waves from CanvasState. Includes top-level Input and Output ports. +/// Waves contain info which will be used later to create the SVGs for those waves actually +/// selected. Init value of these from this function is None. +let getWaves (ws: WaveSimModel) (fs: FastSimulation) : Map = + let start = TimeHelpers.getTimeMs () + //printfn $"{fs.WaveIndex.Length} possible waves" + fs.WaveIndex + |> TimeHelpers.instrumentInterval "getAllPorts" start + |> Array.map (fun wi -> wi, makeWave ws fs wi) + //|> fun x -> printfn $"Made waves!";x + |> Map.ofArray + |> TimeHelpers.instrumentInterval "makeWavePipeline" start + + + + + +/// Generates SVG to display non-binary values on waveforms. +/// Should be refactored together with displayBigIntOnWave. +let displayUInt32OnWave + (wsModel: WaveSimModel) + (width: int) + (waveValues: array) + (transitions: array) + : list = + // find all clock cycles where there is a NonBinaryTransition.Change + let changeTransitions = + transitions + |> Array.indexed + |> Array.filter (fun (_, x) -> x = Change) + |> Array.map (fun (i, _) -> i) + + // find start and length of each gap between a Change transition + let gaps: array = + if Constants.generateVisibleOnly + then + // add dummy change at visible end, but need account for difference in changes: + // e.g. if we are showing 3 cycles, a wave with a change in each would be 0, 1, 2, 3 and would be fine when + // 4 is added; however, a wave with no change at all would be 0, and would produce an errorneous gap length + // of 4 when 4 is added - we therefore add 3 + if changeTransitions[Array.length changeTransitions-1] <> wsModel.ShownCycles + then Array.append changeTransitions [|wsModel.ShownCycles|] + else Array.append changeTransitions [|wsModel.ShownCycles+1|] + |> Array.map (fun loc -> loc+wsModel.StartCycle) // shift cycle to start cycle + else + Array.append changeTransitions [|wsModel.StartCycle+transitions.Length-1|] // add dummry change length end + |> Array.pairwise + |> Array.map (fun (i1, i2) -> {Start = i1; Length = i2-i1}) // get start and length of gap + + // utility functions for SVG generation + /// Function to make polygon fill for a gap. + /// Array of polyline points to fill. + let makePolyfill (points: array) = + let points = points |> Array.distinct + polyline (wavePolyfillStyle points) [] + + /// Function to make text element for a gap. + /// Starting X location of element. + let makeTextElement (start: float) (waveValue: string) = + text (singleValueOnWaveProps start) [ str waveValue ] + + // create text element for every gap + gaps + |> Array.map (fun gap -> + // generate string + let waveValue = UInt32ToPaddedString Constants.waveLegendMaxChars wsModel.Radix width waveValues[gap.Start] + + // calculate display widths + let cycleWidth = singleWaveWidth wsModel + let gapWidth = (float gap.Length * cycleWidth) - 2. * Constants.nonBinaryTransLen + let singleWidth = 1.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue + let doubleWidth = 2. * singleWidth + Constants.valueOnWavePadding + + match gapWidth with + | w when (w < singleWidth) -> // display filled polygon + let fillPoints = nonBinaryFillPoints cycleWidth gap + let fill = makePolyfill fillPoints + [ fill ] + | w when (singleWidth <= w && w < doubleWidth) -> // diplay 1 copy at centre + let gapCenterPadWidth = (float gap.Length * cycleWidth - singleWidth) / 2. + let singleText = makeTextElement (float gap.Start * cycleWidth + gapCenterPadWidth) waveValue + [ singleText ] + | w when (doubleWidth <= w) -> // display 2 copies at end of gaps + let singleCycleCenterPadWidth = // if a single cycle gap can include 2 copies, set arbitrary padding + if cycleWidth < doubleWidth + then (cycleWidth - singleWidth) / 2. + else Constants.valueOnWaveEdgePadding + let startPadWidth = + if singleCycleCenterPadWidth < 0.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue + then 0.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue + else singleCycleCenterPadWidth + let endPadWidth = (float gap.Length * cycleWidth - startPadWidth - singleWidth) + let startText = makeTextElement (float gap.Start * cycleWidth + startPadWidth) waveValue + let endText = makeTextElement (float gap.Start * cycleWidth + endPadWidth) waveValue + [ startText; endText ] + | _ -> // catch-all + failwithf "displayUInt32OnWave: impossible case" + ) + |> List.concat + +/// Generates SVG to display bigint values on waveforms. +/// Should be refactored together with displayUInt32OnWave. +let displayBigIntOnWave + (wsModel: WaveSimModel) + (width: int) + (waveValues: array) + (transitions: array) + : list = + // find all clock cycles where there is a NonBinaryTransition.Change + let changeTransitions = + transitions + |> Array.indexed + |> Array.filter (fun (_, x) -> x = Change) + |> Array.map (fun (i, _) -> i) + + // find start and length of each gap between a Change transition + let gaps: array = + if Constants.generateVisibleOnly + then + // add dummy change at visible end, but need account for difference in changes: + // e.g. if we are showing 3 cycles, a wave with a change in each would be 0, 1, 2, 3 and would be fine when + // 4 is added; however, a wave with no change at all would be 0, and would produce an errorneous gap length + // of 4 when 4 is added - we therefore add 3 + if changeTransitions[Array.length changeTransitions-1] <> wsModel.ShownCycles + then Array.append changeTransitions [|wsModel.ShownCycles|] + else Array.append changeTransitions [|wsModel.ShownCycles+1|] + |> Array.map (fun loc -> loc+wsModel.StartCycle) // shift cycle to start cycle + else + Array.append changeTransitions [|wsModel.StartCycle+transitions.Length-1|] // add dummry change length end + |> Array.pairwise + |> Array.map (fun (i1, i2) -> {Start = i1; Length = i2-i1}) // get start and length of gap + + // utility functions for SVG generation + /// Function to make polygon fill for a gap. + /// Array of polyline points to fill. + let makePolyfill (points: array) = + let points = points |> Array.distinct + polyline (wavePolyfillStyle points) [] + + /// Function to make text element for a gap. + /// Starting X location of element. + let makeTextElement (start: float) (waveValue: string) = + text (singleValueOnWaveProps start) [ str waveValue ] + + // create text element for every gap + gaps + |> Array.map (fun gap -> + // generate string + let waveValue = BigIntToPaddedString Constants.waveLegendMaxChars wsModel.Radix width waveValues[gap.Start] + + // calculate display widths + let cycleWidth = singleWaveWidth wsModel + let gapWidth = (float gap.Length * cycleWidth) - 2. * Constants.nonBinaryTransLen + let singleWidth = 1.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue + let doubleWidth = 2. * singleWidth + Constants.valueOnWavePadding + + match gapWidth with + | w when (w < singleWidth) -> // display filled polygon + let fillPoints = nonBinaryFillPoints cycleWidth gap + let fill = makePolyfill fillPoints + [ fill ] + | w when (singleWidth <= w && w < doubleWidth) -> // diplay 1 copy at centre + let gapCenterPadWidth = (float gap.Length * cycleWidth - singleWidth) / 2. + let singleText = makeTextElement (float gap.Start * cycleWidth + gapCenterPadWidth) waveValue + [ singleText ] + | w when (doubleWidth <= w) -> // display 2 copies at end of gaps + let singleCycleCenterPadWidth = // if a single cycle gap can include 2 copies, set arbitrary padding + if cycleWidth < doubleWidth + then (cycleWidth - singleWidth) / 2. + else Constants.valueOnWaveEdgePadding + let startPadWidth = + if singleCycleCenterPadWidth < 0.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue + then 0.1 * DrawHelpers.getTextWidthInPixels Constants.valueOnWaveText waveValue + else singleCycleCenterPadWidth + let endPadWidth = (float gap.Length * cycleWidth - startPadWidth - singleWidth) + let startText = makeTextElement (float gap.Start * cycleWidth + startPadWidth) waveValue + let endText = makeTextElement (float gap.Start * cycleWidth + endPadWidth) waveValue + [ startText; endText ] + | _ -> // catch-all + failwithf "displayUInt32OnWave: impossible case" + ) + |> List.concat + + + +/// Check if generated SVG is correct, based on existence, position, +/// and zoom. Fast simulation data is assumed unchanged. Used to determine if +/// generateWaveform is run. +let waveformIsUptodate (ws: WaveSimModel) (wave: Wave): bool = + wave.SVG <> None && + wave.ShownCycles = ws.ShownCycles && + wave.StartCycle = ws.StartCycle && + wave.CycleWidth = singleWaveWidth ws && + wave.Radix = ws.Radix && + wave.Multiplier = ws.CycleMultiplier + +/// Called when InitiateWaveSimulation message is dispatched and when wave +/// simulator is refreshed. Generates or updates the SVG for a specific waveform +/// whether needed or not. The SVG depends on cycle width as well as start/stop +/// clocks and design. Assumes that the fast simulation data has not changed and +/// has enough cycles. +let generateWaveform (ws: WaveSimModel) (index: WaveIndexT) (wave: Wave): Wave = + let makePolyline points = + let points = points |> Array.concat |> Array.distinct + polyline (wavePolylineStyle points) [] + + let waveform = + match wave.Width with + | 0 -> + failwithf "Cannot have wave of width 0" + + | 1 -> // binary waveform + let transitions = calculateBinaryTransitionsUInt32 wave.WaveValues.UInt32Step ws.StartCycle ws.ShownCycles ws.CycleMultiplier + + let wavePoints = + let waveWidth = singleWaveWidth ws + let startCycle = if Constants.generateVisibleOnly then ws.StartCycle else 0 + Array.mapi (binaryWavePoints waveWidth startCycle) transitions + |> Array.concat + |> Array.distinct + + svg (waveRowProps ws) [ polyline (wavePolylineStyle wavePoints) [] ] + + | w when w <= 32 -> // non-binary waveform + let transitions = calculateNonBinaryTransitions wave.WaveValues.UInt32Step ws.StartCycle ws.ShownCycles ws.CycleMultiplier + let fstPoints, sndPoints = + let waveWidth = singleWaveWidth ws + let startCycle = if Constants.generateVisibleOnly then ws.StartCycle else 0 + Array.mapi (nonBinaryWavePoints waveWidth startCycle) transitions |> Array.unzip + + let valuesSVG = displayUInt32OnWave ws wave.Width wave.WaveValues.UInt32Step transitions + let polyLines = [makePolyline fstPoints; makePolyline sndPoints] + + svg (waveRowProps ws) (List.append polyLines valuesSVG) + + | _ -> // non-binary waveform with width greather than 32 + let transitions = calculateNonBinaryTransitions wave.WaveValues.UInt32Step ws.StartCycle ws.ShownCycles ws.CycleMultiplier + + let fstPoints, sndPoints = + Array.mapi (nonBinaryWavePoints (singleWaveWidth ws) 0) transitions |> Array.unzip + + let valuesSVG = displayBigIntOnWave ws wave.Width wave.WaveValues.BigIntStep transitions + + svg (waveRowProps ws) (List.append [makePolyline fstPoints; makePolyline sndPoints] valuesSVG) + {wave with + Radix = ws.Radix + ShownCycles = ws.ShownCycles + StartCycle = ws.StartCycle + Multiplier = ws.CycleMultiplier + CycleWidth = singleWaveWidth ws + SVG = Some waveform} + + +/// This function regenerates all the waveforms listed on wavesToBeMade . +/// Generation is subject to timeout, so may not complete. +/// This function have been augmented with performance monitoring function, turn Constants.showPerfLogs +/// to print performance information to console. +/// A tuple with the following information:
+/// a) allWaves (with new waveforms),
+/// b) numberDone (no of waveforms made), and
+/// c) timeToDo (Some timeTaken when greater than timeOut or None +/// if completed with no time out).
+let makeWaveformsWithTimeOut + (timeOut: option) + (ws: WaveSimModel) + (allWaves: Map) + (wavesToBeMade: list) + : Map * int * option = + let start = TimeHelpers.getTimeMs() + let allWaves, numberDone, timeToDo = + ((allWaves, 0, None), wavesToBeMade) + ||> List.fold (fun (all,n, _) wi -> + match timeOut, TimeHelpers.getTimeMs() - start with + | Some timeOut, timeSoFar when timeOut < timeSoFar -> + all, n, Some timeSoFar + | _ -> + (Map.change wi (Option.map (generateWaveform ws wi)) all), n+1, None) + let finish = TimeHelpers.getTimeMs() + if Constants.showPerfLogs then + let countWavesWithWidthRange lowerLim upperLim = + wavesToBeMade + |> List.map (fun wi -> (Map.find wi allWaves).Width) + |> List.filter (fun width -> lowerLim <= width && width <= upperLim) + |> List.length + + printfn "PERF:makeWaveformsWithTimeOut: generating visible only: %b" Constants.generateVisibleOnly + printfn "PERF:makeWaveformsWithTimeOut: making %d/%d waveforms" (List.length wavesToBeMade) (Map.count allWaves) + printfn "PERF:makeWaveformsWithTimeOut: binary = %d" (countWavesWithWidthRange 1 1) + printfn "PERF:makeWaveformsWithTimeOut: int32 = %d" (countWavesWithWidthRange 2 32) + printfn "PERF:makeWaveformsWithTimeOut: process took %.2fms" (finish-start) + + allWaves, numberDone, timeToDo + + + + + + + +