Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TODO 4: Design system #168

Open
8 of 12 tasks
Tracked by #69
nathanjhood opened this issue Dec 13, 2024 · 16 comments · Fixed by #218
Open
8 of 12 tasks
Tracked by #69

TODO 4: Design system #168

nathanjhood opened this issue Dec 13, 2024 · 16 comments · Fixed by #218
Assignees
Labels
design system Changes to the component library, design system, or global panel code

Comments

@nathanjhood nathanjhood self-assigned this Dec 13, 2024
@nathanjhood nathanjhood moved this to In Progress in StoneyDSP Dec 13, 2024
@nathanjhood nathanjhood added design system Changes to the component library, design system, or global panel code and removed panels labels Dec 13, 2024
@nathanjhood nathanjhood changed the title Panels TODO 5: Design system Dec 13, 2024
@nathanjhood nathanjhood changed the title TODO 5: Design system TODO 4: Design system Dec 13, 2024
@nathanjhood
Copy link
Member Author

nathanjhood commented Dec 13, 2024

Thinking out loud about panels:

Fundamental LFO has a width of 9HP - here confirmed by measuring against a row of my HP1 module (pretty much what it was made to do!)

Screenshot from 2024-12-13 04-01-08

I find that a bit weird though because there are 4 ports, but 9HP? We could say 0.5HP either side and we're even, but it isn't really possible to account for 0.5HP if the module were half the width of Fundamental LFO... so, it's just a "by feel" number, I guess, in Fundamental LFO's case.

I imagine it probably looks rigid and uninteresting when everything is very grid-like with equilateral placement, such as if it were 8HP.

Perhaps a good rule of thumb here - for panels generally - will be; Width = (number of ports * 2) + 1

The spare 1 just being a bit of thumb room. Pun intended.

EDIT: Hmmm... looking at it some more, the extra 1 is a helpful offset, which allows the odd-numbered "big knob" (just one of them) to sit flush in the centre of the row of even-numbered ports. Clever.

@nathanjhood
Copy link
Member Author

So if an even-number N of ports fits well into a panel width of N+1... how about an odd number of ports??

If there were three ports, then I'd be inclined to make a 5HP width - so that there can still be a "centre" and just have the ports a little more spread out.

Makes me wonder if odd-numbered panel widths are a bit of a beauty spot... depending on who is beholding the eye.

@nathanjhood nathanjhood linked a pull request Dec 17, 2024 that will close this issue
@github-project-automation github-project-automation bot moved this from In Progress to Done in StoneyDSP Dec 17, 2024
@nathanjhood nathanjhood reopened this Dec 17, 2024
@nathanjhood nathanjhood moved this from Done to In Progress in StoneyDSP Dec 17, 2024
@nathanjhood
Copy link
Member Author

Problem

So. Much. Boilerplate. Primarily, my module code is excessively large because:

  • adding screws
  • adding lines
  • adding widgets
  • adding framebuffers holding the widgets
  • adding the framebuffers to the module widgets
  • drawing panel and panel shine

Idea

Here's something that might work...

  • subclass ::rack::ModuleWidget and add all of the above "trimmings" to it
  • module widgets should subclass the above instead
  • alternatively, some free functions such as void drawPanel(const DrawArgs& args) can probably do the same, but I'd prefer that the calling class actually owns the method/data if possible

Bonus points

  • use logic on this->size to determine number of screws and lines???
  • drive the panel shine's color opacity value with ::rack::settings::rackBrightness to make the panels "more dull"
  • create an internal-only "DesignSystem" module as part of the plugin, which I can keep around in my Rack sessions as a "palette" for widget designs and ideas, experiments, etc, strictly to be disabled from the deployment builds

@nathanjhood
Copy link
Member Author

vague plan

  • make "DesignSystem" module which is internal-only and kept out of production builds
  • use this to design a set of Widget sub-classes which will be major components of the StoneyVCV design system itself
  • Some widgets we need:
    • knobs of each of the 5 sizes, with underlays for panel labels and knob rings
    • in and out ports with panel labels, and underlays which indicate visually whether the port is an input or output, per VCV's Panel design suggestions
    • Lights which we like but maybe won't need until later, such as off>green>yellow>red signal meters and bipolar signal themes (blue for negative voltages, green for positive? etc)
    • trying using the screw-and-line module wrapper as a smaller sub-widget within the panels, to make parts appear to be "serviceable" from the front (I'd offer in this in certain cases if this were hardware too)
    • figure out if making a "look-and-feel" base class which can be subclassed by all of our modules, works correctly
    • if it does, then strip the module code right down to remove all repetition and replace with this base class inheritance

In the process, I hope to also start and roughly finish the LFO module. This clears the way for some of the more interesting "main event" modules I have in mind, which I will broadly work on over the course of the coming months/year.

I might attempt to push my control modules and blank panels as a plugin to the user library meanwhile, as a placeholder for the brand name, and for the sake of investigation.

@nathanjhood
Copy link
Member Author

nathanjhood commented Jan 3, 2025

Into the nuts and bolts and backplates now...

Dilemma

This (port socket backplate) is a nightmare to work with:

Screenshot from 2025-01-03 15-35-07
Screenshot from 2025-01-03 15-34-48

Really difficult in code because the coloured backpanel (widget) is not coupled with (or a child of) the port (widget). Both the port socket, and the backplate are being set separately using hard-coded pixel values, which is a design-system no-no...

Really difficult in Inkscape because Inkscape...

To make matters even more complex, ports are of course circular, meaning we either work with their centred co-ordinates with some maths, or work with a leading edge, and some maths.

Solution

Sub-class rack::componentlibrary::ThemedPort into a new struct which adds the themed backpanel as a childBelow itself....

Such a sub-class must be compatible with rack::createInputCentred and rck::createOutputCentred [sic]

@nathanjhood
Copy link
Member Author

nathanjhood commented Jan 3, 2025

Screenshot from 2025-01-03 15-35-07 Screenshot from 2025-01-03 15-34-48

VCA Module Widget constructor output port member initializer:

portVcaOutput(
        ::rack::createOutputCentered<::rack::componentlibrary::ThemedPJ301MPort>(
            ::rack::math::Vec(
                size.x * 0.5F,
                // widget is 28.55155 x 39.15691
                // port is 23.7 x 23.7
                // widget.x - port.x = 4.85155 (/ 2 = 2.425775 = edge distance)
                ((39.15691F - (23.7F * 0.5F)) - 2.425775F) + (309.05634F)
            ),
            module,
            ::StoneyDSP::StoneyVCV::VCA::VCAModule::IdxOutputs::VCA_OUTPUT
        )
    )

VCA Module Widget draw() snippet for backpanel:

void draw(const DrawArgs& args)
{
    // <SNIP>

    // draw out port box
    ::nvgBeginPath(args.vg);
    ::nvgRoundedRect(args.vg,
        /** x */(size.x * 0.5F) - (tst.x * 0.5F),
        /** y */(309.05634F),
        /** w */tst.x,
        /** h */tst.y,
        /** rx */2.83465F
    );
    ::nvgFillColor(args.vg, bgPort);
    ::nvgFill(args.vg);

    return ::rack::Widget::draw(args);
}

Rack internal code:

template <class TPortWidget>
TPortWidget* createInput(math::Vec pos, engine::Module* module, int inputId) {
	TPortWidget* o = new TPortWidget;
	o->box.pos = pos;
	o->app::PortWidget::module = module;
	o->app::PortWidget::type = engine::Port::INPUT;
	o->app::PortWidget::portId = inputId;
	return o;
}


template <class TPortWidget>
TPortWidget* createInputCentered(math::Vec pos, engine::Module* module, int inputId) {
	TPortWidget* o = createInput<TPortWidget>(pos, module, inputId);
	o->box.pos = o->box.pos.minus(o->box.size.div(2));
	return o;
}

We can remove many of those hard-coded decimal values (xx.xxxxxF) if the sub-class idea works....

@nathanjhood
Copy link
Member Author

Ok it's quite easy... just one new widget, sub-classing Rack's ThemedPort and over-riding draw():

Screenshot from 2025-01-03 18-01-15
Screenshot from 2025-01-03 18-00-52

Now to figure out the correct placement, then add a label (optional) and some sort of logic to switch the back panel part (blue, in the above) off (optional).

@nathanjhood
Copy link
Member Author

Big win:

Screenshot from 2025-01-03 18-34-05
Screenshot from 2025-01-03 18-33-51

@nathanjhood
Copy link
Member Author

Totally worth it. I just cut the VCA code way down and got themed port panels:

Screenshot from 2025-01-03 18-43-11
Screenshot from 2025-01-03 18-42-56

@nathanjhood
Copy link
Member Author

nathanjhood commented Jan 4, 2025

Labelled and themed input and output ports are live:

Screenshot from 2025-01-04 18-17-38

Screenshot from 2025-01-04 18-26-08

Gonna make one big, messy commit to get this into production shortly.

@nathanjhood
Copy link
Member Author

I have a slider version (instead of knob) which I think I prefer for the VCA, or possibly in general... incoming.

@nathanjhood
Copy link
Member Author

nathanjhood commented Jan 19, 2025

WIP status: working design, text labels for RoundBlackKnob:

Screenshot from 2025-01-15 06-22-52
Screenshot from 2025-01-15 06-22-33
Screenshot from 2025-01-15 06-21-39
Screenshot from 2025-01-15 06-21-10

Since we can just resize the previously-known-as "opaque type" object anyway with setSize(), there isn't any need to make one panel for each size of knob; just access the element in the Vector as needed.

Here, I used the exact same Knob Panel struct, simply swapped RoundSmallBlackKnob for RoundHugeBlackKnob, and passed the gainKnob widget instance's size to the Knob Panel's setSize(), and the text adjusted accordingly:

Screenshot from 2025-01-15 07-29-02
Screenshot from 2025-01-15 07-28-36
Screenshot from 2025-01-15 07-27-51
Screenshot from 2025-01-15 07-27-13

So, the cropped outer ring should be able to work the same!

Still to come: add the outer ring, including an (enum?) accessor to specify whether the Knob is unipolar, or bipolar.

Extra points if we can deduce the crop angle of the outer ring using the minValue and maxValue members...

WIP: Added a cutaway with the (one-armed) scissor:

Screenshot from 2025-01-15 23-28-21
Screenshot from 2025-01-15 23-28-07
Screenshot from 2025-01-15 23-26-49
Screenshot from 2025-01-15 23-26-33

I believe the slightly ugly flat-top of the clipping could be improved, by replacing this scissor method - whereby I'm simply cropping the viewbox by some constant amount(s) - with another hollow circle, moved away from the centre towards the clipping region.

Nonetheless, I've sure seen (and done) much worse than this attempt. In some sense, it's good enough to pass.

WIP update: using nvgArc with rounded linecap for the win:

Image
Image
Image
Image

The values of nvgArc are exactly the values taken from the knob widget's min and max, and it's quite simple overall.

So, to prove a point... let's try throwing some weird min/max values and different knob sizes - no other changes - while cleaning up the debris of unused code I've got locally (some of which I'll probably put in the StoneyDSP lib as it's useful nonetheless, if not here).

EDIT: update

So, to prove a point...

...I:

  • reduced the knob min and max values arbitrarily
  • changed Widget variation from Huge to Small

Result:

Image
Image

The knob ring is perfectly tracking the parent widget's range! 🆒

The margin, or "leading" seems a bit "wrong" here, too much of it for the smaller widget size... but since that variable can be easily set externally, it's no show-stopper; I've already accomplished more here than I actually expected to.

EDIT: Mission accomplished:

Close-up showing rounded linecaps and corners, and nice even spacings:

Screenshot from 2025-01-18 06-44-13
Screenshot from 2025-01-18 06-43-59
Screenshot from 2025-01-18 06-43-43
Screenshot from 2025-01-18 06-43-25
Screenshot from 2025-01-18 06-42-23
Screenshot from 2025-01-18 06-42-07

(VCA module still in draft mode here):

Screenshot from 2025-01-18 07-16-16
Screenshot from 2025-01-18 07-15-58

👍

@nathanjhood
Copy link
Member Author

Latest: I was able to draught this LFO panel up very quickly (comparatively speaking) within nanoVG, using the new design system and component library Widgets:

Image
Image

Plenty missing, and some obvious thoughts straight off the bat (port spacing?) but lots of great things "just worked":

  • lines and (themed) screws automatically placed and drawn
  • knob ring from min to max values of the knob's range
  • knob is labelled and the text is not obfuscated by the size of the knob
  • ports are clearly outputs thanks to the ThemedPortPanelWidget
  • port panels merge nicely when close enough (though too close here, tbh)
  • ports are labelled
  • all the above responds to light/dark theme changes and already uses the global theme (text and BG colors, font, lighting/gradient...)
  • one framebuffer holding most of the above

There are a couple more smells under the hood, particularly in Module and ModuleWidget constructor code...

We might be too far in to the LFO already to fish those things out effectively while developing this module; I'll probably sprint to get a few more of this module's features in place before anything else.

@nathanjhood
Copy link
Member Author

Interesting - and, expected - challenge with the panel spacing:

Image
Image
Image
Image

I don't like the port panels being flush against the panel bounds (inner lines) - I think in a hardware scenario, those areas would be a little bit flimsy without the right care, and hence I'm using those inner lines to tell myself what my "safe margin" is, at least according to what's in my head about hardware panels.

In this case, I'll probably reduce the gap just enough and see if I can condense the port panels into one, without squishing up the thumb room for handling cables. If necessary, I'll add some sort of variable to set corner types on the port panel widgets....

@nathanjhood
Copy link
Member Author

nathanjhood commented Jan 20, 2025

There are a couple more smells under the hood, particularly in Module and ModuleWidget constructor code...

I rooted this one out.

In summary; since the std::vectors various widget types are specified some levels of inheritance away from the final module, I had overlooked something which was causing runtime exceptions whenever I tried to access elements at position 0 on any of these arrays.

The problem was, that I have specified an in-class initialiser to a null-ish value on each of these vectors. This null-ish value at position 0 was making its' way all the way to the instantiated module constructor, where I was mistakenly thinking I was reserving and emplacing new elements into an empty vector, having overlooked that this null-ish value was already in there... d'oh!

Just adding vec.clear() before the emplacing of elements allows us to massively reduce all of that smelly constructor code down into proper, normal/sane iterable semantics instead. Which was of course the original idea - i Just got waylaid by tat unexpected runtime exception on element position 0 for a minute....

There are still some cases where specific element access is desired - setting labels and other ephemeral, unique options - and that is completely reasonable.

I'll update the VCA module in due course, and the blank panels eventually (if at all).

@nathanjhood
Copy link
Member Author

nathanjhood commented Jan 20, 2025

What I really want to do is...

(pseudo-code)

  • Add a data member to the <name>Widget structs' parent class, which is a reference to a ::rack::ModuleWidget
  • when constructing/initializing <name>Widget, pass the owning ::rack::ModuleWidget to the constructor by reference
  • use this live reference to the owning struct, to organize all the panel layout within the <name>Widget constuctor
  • for example, all the panels for ports and knobs could all be done directly in one place, rather than two

The blocker:

  • need a friend function for creating the <name>Widget which accepts the ::rack::ModuleWidget by reference, and does not interfere with any existing functions or implementations (probably should be something like createPanelWidget(const ModuleWidget &mw))
  • in our ModuleWidget constructors, replace the generic createWidget<{name}Widget> with the above
  • Rack already has createPanel but this does a bunch of extra stuff - some we deffo want, some we don't need - and accepts only an SVG via shared pointer
  • I'm also considering sub-classing ::rack::ModuleWidget as part of the design system... and still pondering how that might affect my needs stated above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design system Changes to the component library, design system, or global panel code
Projects
Status: In Progress
Development

Successfully merging a pull request may close this issue.

1 participant