SoMField(3)

πŸ“¦ version 2.5.0, in fedora 43, fedora 44

πŸ’– SoMField(3) β€” the manual page, bestie πŸ’–

okay so welcome to the manual page for SoMField, and she is that girl β€” the base class for fields that can hold multiple values. This is part 1 of 3, so don't go anywhere. πŸ’…βœ¨


🌸 NAME 🌸

SoMField β€” the base class for fields which can contain multiple values. That's the whole assignment. She's the mother of the multi-value field family, and she ate. Periodt. πŸ‘‘


✨ SYNOPSIS ✨

#include <Inventor/fields/SoMField.h>

Inherits SoField.

Inherited by: SoMFBool, SoMFColor, SoMFColorRGBA, SoMFDouble, SoMFEngine, SoMFEnum, SoMFFloat, SoMFInt32, SoMFMatrix, SoMFName, SoMFNode, SoMFPath, SoMFPlane, SoMFRotation, SoMFShort, SoMFString, SoMFTime, SoMFUInt32, SoMFUShort, SoMFVec2b, SoMFVec2d, SoMFVec2f, SoMFVec2i32, SoMFVec2s, SoMFVec3b, SoMFVec3d, SoMFVec3f, SoMFVec3i32, SoMFVec3s, SoMFVec4b, SoMFVec4d, SoMFVec4f, SoMFVec4i32, SoMFVec4s, SoMFVec4ub, SoMFVec4ui32, and SoMFVec4us. the whole squad is here πŸ’–


πŸ’… Public Member Functions πŸ’…

here's her own public squad, every single one:

  • virtual ~SoMField () β€” destructor duties, as always. πŸ’€
  • int getNum (void) const β€” gives you the count. πŸ”’
  • void setNum (const int num) β€” sets the count to num. ✨
  • virtual void deleteValues (int start, int num=-1) β€” deletes values from start, with num defaulting to -1. πŸ—‘οΈ
  • virtual void insertSpace (int start, int num) β€” inserts space at start for num values. πŸͺ„
  • SbBool set1 (const int index, const char *const valuestring) β€” sets a single value at index from valuestring. πŸ’–
  • void get1 (const int index, SbString &valuestring) β€” gets a single value at index into valuestring. πŸŽ€
  • virtual void enableDeleteValues (void) β€” enables delete-values. πŸ”₯
  • virtual SbBool isDeleteValuesEnabled (void) β€” asks if delete-values is enabled. πŸ‘‘

🫢 Public Member Functions inherited from SoField 🫢

these are all the public functions she inherited from SoField, every single one, no cap:

  • virtual ~SoField () πŸ’€
  • void setIgnored (SbBool ignore) πŸ™ˆ
  • SbBool isIgnored (void) const πŸ‘€
  • void setDefault (SbBool def) ⭐
  • SbBool isDefault (void) const πŸ’«
  • virtual SoType getTypeId (void) const =0 πŸ†”
  • SbBool isOfType (const SoType type) const πŸ”
  • void enableConnection (SbBool flag) πŸ”—
  • SbBool isConnectionEnabled (void) const πŸ”Œ
  • SbBool connectFrom (SoEngineOutput *master, SbBool notnotify=0, SbBool append=0) βš™οΈ
  • SbBool appendConnection (SoEngineOutput *master, SbBool notnotify=0) βž•
  • void disconnect (SoEngineOutput *engineoutput) βœ‚οΈ
  • SbBool isConnectedFromEngine (void) const πŸš‚
  • SbBool getConnectedEngine (SoEngineOutput *&master) const πŸ› οΈ
  • SbBool connectFrom (SoField *master, SbBool notnotify=0, SbBool append=0) πŸ”—
  • SbBool appendConnection (SoField *master, SbBool notnotify=0) βž•
  • void disconnect (SoField *field) βœ‚οΈ
  • SbBool isConnectedFromField (void) const 🌾
  • SbBool getConnectedField (SoField *&master) const 🧲
  • int getNumConnections (void) const πŸ”’
  • int getForwardConnections (SoFieldList &slavelist) const ⏩
  • int getConnections (SoFieldList &masterlist) const πŸ”
  • void disconnect (void) βœ‚οΈ
  • SbBool isConnected (void) const πŸ”—
  • void setContainer (SoFieldContainer *cont) πŸ“¦
  • SoFieldContainer * getContainer (void) const 🎁
  • SbBool set (const char *valuestring) πŸ“
  • void get (SbString &valuestring) πŸ“€
  • SbBool shouldWrite (void) const ✍️
  • virtual void touch (void) πŸ‘†
  • virtual void startNotify (void) πŸ””
  • virtual void notify (SoNotList *nlist) πŸ“£
  • SbBool enableNotify (SbBool on) πŸ”•
  • SbBool isNotifyEnabled (void) const πŸ””
  • void addAuditor (void *f, SoNotRec::Type type) πŸ‘₯
  • void removeAuditor (void *f, SoNotRec::Type type) πŸšͺ
  • int operator== (const SoField &f) const 🟰
  • int operator!= (const SoField &f) const ❌
  • virtual void connectionStatusChanged (int numconnections) πŸ”„
  • SbBool isReadOnly (void) const πŸ”’
  • virtual SbBool isSame (const SoField &f) const =0 🟰
  • virtual void copyFrom (const SoField &f)=0 πŸ“‹
  • virtual void fixCopy (SbBool copyconnections) πŸ› οΈ
  • virtual SbBool referencesCopy (void) const 🧾
  • void copyConnection (const SoField *fromfield) πŸ”—
  • virtual SbBool read (SoInput *in, const SbName &name) πŸ“–
  • virtual void write (SoOutput *out, const SbName &name) const ✍️
  • virtual void countWriteRefs (SoOutput *out) const πŸ”’
  • void setFieldType (int type) 🏷️
  • int getFieldType (void) const 🏷️
  • SbBool getDirty (void) const 🧼
  • void setDirty (SbBool dirty) 🧽
  • void evaluate (void) const 🧠

✨ Static Public Member Functions ✨

  • static SoType getClassTypeId (void) β€” gives you the class type ID. πŸ†”
  • static void initClass (void) β€” initializes the class. πŸš€

πŸ’– Static Public Member Functions inherited from SoField πŸ’–

  • static void initClass (void) πŸš€
  • static void initClasses (void) πŸš€
  • static SoType getClassTypeId (void) πŸ†”

πŸŽ€ Protected Member Functions πŸŽ€

  • SoMField (void) β€” the protected constructor. πŸ›‘οΈ
  • virtual void makeRoom (int newnum) β€” makes room for newnum. πŸͺ„

🫢 Protected Member Functions inherited from SoField 🫢

  • SoField (void) πŸ›‘οΈ
  • void valueChanged (SbBool resetdefault=1) πŸ”„
  • virtual void evaluateConnection (void) const 🧠
  • virtual SbBool readConnection (SoInput *in) πŸ“–
  • virtual void writeConnection (SoOutput *out) const ✍️
  • SbBool isDestructing (void) const πŸ’€

🌈 Protected Attributes 🌈

  • int num πŸ”’
  • int maxNum πŸ”’
  • SbBool userDataIsUsed πŸ“¦

πŸ¦‹ Additional Inherited Members πŸ¦‹

πŸ‘‘ Public Types inherited from SoField πŸ‘‘

  • enum FieldType { NORMAL_FIELD = 0, EVENTIN_FIELD, EVENTOUT_FIELD, EXPOSED_FIELD } 🏷️

πŸ’– Detailed Description πŸ’–

okay so SoMField is the base class for fields that can hold more than one value. it's giving mother of all multivalue fields. πŸ‘‘

every field type that can contain more than one member value inherits from this class, and heads up: SoMField is an abstract class. so like, you can't just instantiate it, bestie, it's the blueprint not the house. 🏠


🌸 how to actually use it

Use setValue(), setValues() or set1Value() to set the values of fields that inherit SoMField, and use getValues() or the index operator [] to read values. example code time, pull up a chair:

SoText2 * textnode = new SoText2;
textnode->ref();

// Setting multi-field values. /////////////////////////////

// Set the first value of the SoMFString field of the SoText2 node.
// The field array will be truncated to only contain this single value.
textnode->string.setValue("Morten");
// Full contents of the SoMFString is: [ "Morten" ]

// The setValue() method and the = operator is interchangeable,
// so this code line does the same as the previous line.
textnode->string = "Peder";
// Full contents of the SoMFString is now: [ "Peder" ]

// Set the value at index 2. If the field value array contained
// less than 3 elements before this call, first expand it to contain
// 3 elements.
textnode->string.set1Value(2, "Lars");
// Full contents of the SoMFString is: [ "Peder", <undefined>, "Lars" ]

// This sets 3 values of the array, starting at index 5. If the
// array container had less than 8 elements before the setValues()
// call, the array will first be expanded.
SbString s[3] = { "Eriksen", "Blekken", "Aas" };
textnode->string.setValues(5, sizeof(s) / sizeof(s[0]), s);
// Full contents of the SoMFString is now:
//     [ "Peder", <undefined>, "Lars", <undefined>, <undefined>,
//       "Eriksen", "Blekken", "Aas" ]

// Note also that the setValues() call will *not* truncate a field
// container if you use it to change a subset at the start:
SbString n[4] = { "Dixon", "Adams", "Bould", "Winterburn" };
textnode->string.setValues(0, sizeof(n) / sizeof(n[0]), n);
// Full contents of the SoMFString is now:
//     [ "Dixon", "Adams", "Bould", "Winterburn", <undefined>,
//       "Eriksen", "Blekken", "Aas" ]

// Inspecting multi-field values. //////////////////////////

// This will read the second element (counting from zero) if the
// multivalue field and place it in "val".
SbString val = textnode->string[2];

// Gives us a pointer to the array which the multiple-value field
// is using to store the values. Note that the return value is
// "const", so you can only read from the array, not write to
// it.
const SbString * vals = textnode->string.getValues(0);

// Modifying multi-field values. ///////////////////////////

// You can of course modify multifield-values by using the set-
// and get-methods shown above, but when you're working with
// big sets of data, this will be ineffective. Then use this
// technique instead:
SbString * modvals = textnode->string.startEditing();

// ... lots of modifications to the "modvals" array here ...

// Calling the finishEditing() method is necessary for the
// scene graph to be updated (and re-rendered).
textnode->string.finishEditing();

✨ startEditing / finishEditing, and why it's the move

the reason it's more effective to wrap a bunch of modifications inside startEditing() / finishEditing() is that you avoid the stream of notification messages that would otherwise get sent for every single modification you do. instead there's just one notification about the changes, fired off by the finishEditing() call. one text instead of a hundred. the group chat stays quiet. 🀫


🧚 pre-allocating values

the correct way to pre-allocate a specific number of field values in one chunk is to use the SoMField::setNum() method β€” for instance, in advance of using the startEditing() and finishEditing() methods. heads up though: the field values will be uninitialized after expanding the field with the setNum() call. so they're placeholders, not cute little zeroes. πŸ’€

⚠️ be aware that your application code must be careful to not do silly things during a setValues()-triggered notification. like, the notification is happening while you're mid-change, so don't be weird about it.

if you have code that looks for instance like this:

// update set of coordinate indices at the start of e.g.
// an SoIndexedFaceSet's coordIndex field..
ifs->coordIndex.setValues(0, runner->numIndices, runner->indices);
// ..then truncate to make sure it's the correct size.
ifs->coordIndex.setNum(runner->numIndices);

since setValues() might leave some elements at the end of the array that typically can be invalid indices after that first statement runs, something can go wrong during notification if you've got application code monitoring changes β€” and that application code then for instance triggers an action or something that tries to use the coordIndex field before it's been updated to its correct size with the setNum() call. 🫠 the vibe check fails. it's giving race condition.

(notification can in this case, as always, be temporarily disabled to be on the safe side:

somefield.enableNotify(FALSE);
somefield.setValues(...);
somefield.setNum(...);
somefield.enableNotify(TRUE);
somefield.touch();

this will guarantee that the setValues() and setNum() pair gets executed as an atomic operation. slay. πŸ’…)


πŸ“„ how multivalue fields get written to file

when nodes, engines or other types of field containers are written to file, their multiple-value fields are written to file in this format:

containerclass {
  fieldname [ value0, value1, value2, ...]
  ...
}

..like this, for instance β€” a Coordinate3 node providing 6 vertex coordinates in the form of SbVec3f values in its "point" field for e.g. a faceset, lineset or pointset:

Coordinate3 {
   point [
      -1 1 0, -1 -1 0, 1 -1 0,
      0 2 -1, -2 0 -1, 0 -2 -1,
   ]
}

πŸ”— setValuesPointer() and data sharing

some fields support application data sharing through a setValuesPointer() method. setValuesPointer() makes it possible to set the data pointer directly in the field. normally (when you use setValues()), Coin makes a copy of your data β€” so this method can be super useful if your app needs the data internally and you're just using Coin for the visualization. no double-storing, no wasted memory. icon behavior. πŸ‘‘

myapp->calculateCoordinates(SOME_LARGE_VALUE);
SbVec3f * mycoords = myapp->getCoordinates();

SoCoordinate3 * mynode = myapp->getCoordinateNode();
mynode->point.setValuesPointer(SOME_LARGE_VALUE, mycoords);
⚠️ be aware that your field should be a read-only field when you set the data like this. if you write to the field, the values in your application array will be overwritten. and if you append values to the field, a new array will be allocated, and the data will be copied into it before appending the new values β€” the array pointer will then be discarded. so like, don't be writing to it, bestie, it's not that kind of field. 🚫

also note that whenever you change some value(s) in the array, you must manually notify Coin about this by calling SoField::touch(). for our example:

SbVec3f * mycoords = myapp->getCoordinate();
myapp->updateCoordinate(mycoords);
SoCoordinate3 * mynode = myapp->getCoordinateNode();
mynode->point.touch(); // this will notify Coin that field has changed

πŸ—‘οΈ enableDeleteValues() β€” read the warning, bestie

you can use SoMField::enableDeleteValues() to make Coin delete the array for you when the field is destructed, or when the array pointer is discarded because it isn't needed anymore (e.g. when the array size is changed). the array will be deleted using the C++ delete[] operator, so if you use it, your array must be allocated using the C++ new[] operator.

🚨 SoMField::enableDeleteValues() is supported only to be compatible with later versions of TGS Inventor and we don't recommend using it. it can have undefined results on the Microsoft Windows platform. allocating memory in the application and destructing it in a DLL can be a bad thing, causing mysterious crashes, if you're not very careful about how your application and DLLs are linked to the underlying C library. πŸ’€πŸ’€πŸ’€ no cap, just don't.

πŸ‘€ See also

  • SoSField

πŸŽ€ Constructor & Destructor Documentation

SoMField::~SoMField () [virtual]

destructor in SoMField does nothing. resource deallocation needs to be done from subclasses. so like, clean up after yourself, this base class is not doing it for you. 🧹✨

SoMField::SoMField (void ) [protected]

constructor. initializes number of values in field to zero. a fresh start, a blank slate, main character energy from the very first line. 🌱

References maxNum, num, and userDataIsUsed.

✨ Member Function Documentation ✨

int SoMField::getNum(void) const [inline]

Bestie. It literally just tells you how many values are chillin' in this field. That's the whole job. πŸ’…

References: SoField::evaluate(), and num.

Referenced by (aka everybody and their cousin calls this, no cap):

  • SoMFNode::addNode()
  • SoIndexedShape::computeBBox()
  • SoVRMLIndexedLine::computeBBox()
  • SoVRMLIndexedShape::computeBBox()
  • SoVRMLVertexPoint::computeBBox()
  • SoNonIndexedShape::computeCoordBBox()
  • SoMFString::deleteText()
  • SoDirectionalLightManip::fieldSensorCB()
  • SoPointLightManip::fieldSensorCB()
  • SoSpotLightManip::fieldSensorCB()
  • SoMFNode::findNode()
  • SoMFEngine::fixCopy()
  • SoMFNode::fixCopy()
  • SoMFPath::fixCopy()
  • SoNonIndexedShape::fixNumVerticesPointers()
  • SoIndexedTriangleStripSet::generateDefaultNormals()
  • SoVRMLIndexedFaceSet::generateDefaultNormals()
  • SoVRMLPointSet::generatePrimitives()
  • SoMFNode::getNode()
  • SoMFNode::getNumNodes()
  • SoPointSet::getPrimitiveCount()
  • SoVRMLVertexPoint::getPrimitiveCount()
  • SoVRMLVertexShape::getVertexData()
  • SoMFNode::insertNode()
  • SoMFPath::notify()
  • SoMFEngine::referencesCopy()
  • SoMFNode::referencesCopy()
  • SoMFPath::referencesCopy()
  • SoMFNode::removeNode(), and
  • SoMFNode::replaceNode().

void SoMField::setNum(const int numarg)

Sets the number of values to num. Glow-up or glow-down, your call. πŸ¦‹

If the current number of values is larger than num, the array of values gets truncated from the end (bye, extra values, we hardly knew ye). But if num is larger, the array automatically gets expanded β€” and heads up, random values will be set for the new array items. Random. Not zeroed, not cute little placeholders. Random. 🎲

References: deleteValues(), insertSpace(), and num.

Referenced by:

  • SoTabPlaneDragger::reallyAdjustScaleTabSize()
  • SoMFNode::removeAllNodes()
  • SoMFVec2b::setValue()
  • SoMFVec2d::setValue()
  • SoMFVec2f::setValue()
  • SoMFVec2i32::setValue()
  • SoMFVec2s::setValue()
  • SoMFVec3b::setValue()
  • SoMFVec3d::setValue()
  • SoMFVec3f::setValue()
  • SoMFVec3i32::setValue()
  • SoMFVec3s::setValue()
  • SoMFVec4b::setValue()
  • SoMFVec4d::setValue()
  • SoMFVec4f::setValue()
  • SoMFVec4i32::setValue()
  • SoMFVec4s::setValue()
  • SoMFVec4ub::setValue()
  • SoMFVec4ui32::setValue(), and
  • SoMFVec4us::setValue().

void SoMField::deleteValues(int start, int numarg = -1) [virtual]

Removes value elements from index start up to and including index start + num - 1. Inclusive, bestie β€” that last one is going too. βœ‚οΈ

Elements with indices larger than the last deleted element get moved downwards in the value array (they scootch on over to fill the gap, very polite of them).

If num equals -1, it deletes from index start all the way to the end of the array. Nuke it from start, basically. πŸ’€

Reimplemented in SoMFEngine, SoMFNode, and SoMFPath.

References: num, SoDebugError::post(), and SoField::valueChanged().

Referenced by: SoMFString::deleteText(), and setNum().


void SoMField::insertSpace(int start, int numarg) [virtual]

Inserts num "slots" for new value elements starting from start. The elements already present from start onward get moved "upward" in the extended array β€” everybody shuffles up to make room, like when your friend joins the group photo. πŸ“Έ

Reimplemented in SoMFEngine, SoMFNode, and SoMFPath.

References: num, SoDebugError::post(), and SoField::valueChanged().

Referenced by: setNum().


SbBool SoMField::set1(const int index, const char *const valuestring)

Sets the value at index to the value contained in valuestring. Returns TRUE if a valid value for this field can be extracted from valuestring, otherwise FALSE. So like, vibe check on the string first, then commit. 🫢

If index is larger than the current number of elements in the field, this method will automatically expand the field to accommodate the new value. It's giving generous. ✨

References: maxNum, num, SoInput::setBuffer(), and SoField::valueChanged().


void SoMField::get1(const int index, SbString & valuestring)

Returns the value at index inside the valuestring string. Handing it over, no drama. πŸŽ€

References: SoField::countWriteRefs(), SoOutput::getBuffer(), SoOutput::setBuffer(), SoOutput::setStage(), and SoOutput::write().


void SoMField::enableDeleteValues(void) [virtual]

Can be used to make Coin delete the array pointer that was set through a setValuesPointer() call. See the SoMField documentation for the info about the setValuesPointer() function.

⚠️ Okay so real talk: this method is a TGS extension (introduced in TGS OIV v3.0) and is supported only for compatibility. We suggest that you don't use it, since it can lead to hard-to-find bugs. Like, genuinely hard-to-find. Don't do it. πŸ™ˆ

Since:

  • Coin 2.0
  • TGS Inventor 3.0

References: userDataIsUsed.


SbBool SoMField::isDeleteValuesEnabled(void) [virtual]

Returns whether SoMField::enableDeleteValues() has been called on a field. The result is only valid if setValuesPointer() has been called on the field first β€” order matters, bestie. 🌸

⚠️ Also a TGS extension (introduced in TGS OIV v3.0), supported only for compatibility. Again: we suggest you don't use it, because hard-to-find bugs are the worst kind. 😭

Since:

  • Coin 2.0
  • TGS Inventor 3.0

References: userDataIsUsed.


void SoMField::makeRoom(int newnum) [protected], [virtual]

Makes room in the field to store newnum values. Interior decorating, but for memory. 🧚

References: num.

Referenced by: SoMFColor::setHSVValues(), SoMFColorRGBA::setHSVValues(), SoMFColor::setValues(), and SoMFColorRGBA::setValues().


πŸ’– Member Data Documentation πŸ’–

int SoMField::num [protected]

Number of available values. The count. The receipts. πŸ”₯

Referenced by: SoMFEngine::deleteValues(), deleteValues(), SoMFNode::deleteValues(), SoMFPath::deleteValues(), getNum(), SoMFColor::getValues(), SoMFColorRGBA::getValues(), SoMFEnum::getValues(), SoMFName::getValues(), SoMFRotation::getValues(), SoMFString::getValues(), SoMFVec2b::getValues(), SoMFVec2d::getValues(), SoMFVec2f::getValues(), SoMFVec2i32::getValues(), SoMFVec2s::getValues(), SoMFVec3b::getValues(), SoMFVec3d::getValues(), SoMFVec3f::getValues(), SoMFVec3i32::getValues(), SoMFVec3s::getValues(), SoMFVec4b::getValues(), SoMFVec4d::getValues(), SoMFVec4f::getValues(), SoMFVec4i32::getValues(), SoMFVec4s::getValues(), SoMFVec4ub::getValues(), SoMFVec4ui32::getValues(), SoMFVec4us::getValues(), SoMFEngine::insertSpace(), insertSpace(), SoMFNode::insertSpace(), SoMFPath::insertSpace(), makeRoom(), set1(), SoMFColor::setHSVValues(), SoMFColorRGBA::setHSVValues(), setNum(), SoMFColor::setValues(), SoMFColorRGBA::setValues(), SoMFName::setValues(), SoMFRotation::setValues(), SoMFString::setValues(), SoMFVec2b::setValues(), SoMFVec2d::setValues(), SoMFVec2f::setValues(), SoMFVec2i32::setValues(), SoMFVec2s::setValues(), SoMFVec3b::setValues(), SoMFVec3d::setValues(), SoMFVec3f::setValues(), SoMFVec3i32::setValues(), SoMFVec3s::setValues(), SoMFVec4b::setValues(), SoMFVec4d::setValues(), SoMFVec4f::setValues(), SoMFVec4i32::setValues(), SoMFVec4s::setValues(), SoMFVec4ub::setValues(), SoMFVec4ui32::setValues(), SoMFVec4us::setValues(), and SoMField().


int SoMField::maxNum [protected]

Number of array "slots" allocated for this field. Capacity vs. actual β€” num is who showed up, maxNum is how many chairs there are. πŸ‘‘

Referenced by: set1(), SoMFColor::setHSVValues(), SoMFColorRGBA::setHSVValues(), SoMFColor::setValues(), SoMFColorRGBA::setValues(), SoMFName::setValues(), SoMFRotation::setValues(), SoMFString::setValues(), SoMFVec2b::setValues(), SoMFVec2d::setValues(), SoMFVec2f::setValues(), SoMFVec2i32::setValues(), SoMFVec2s::setValues(), SoMFVec3b::setValues(), SoMFVec3d::setValues(), SoMFVec3f::setValues(), SoMFVec3i32::setValues(), SoMFVec3s::setValues(), SoMFVec4b::setValues(), SoMFVec4d::setValues(), SoMFVec4f::setValues(), SoMFVec4i32::setValues(), SoMFVec4s::setValues(), SoMFVec4ub::setValues(), SoMFVec4ui32::setValues(), SoMFVec4us::setValues(), and SoMField().


SbBool SoMField::userDataIsUsed [protected]

Is TRUE if data has been set through a setValuesPointer() call and set to FALSE through an enableDeleteValues() call. A little boolean keeping track of who's responsible for the memory. πŸ“

Referenced by: enableDeleteValues(), isDeleteValuesEnabled(), and SoMField().


🌈 Author 🌈

Generated automatically by Doxygen for Coin from the source code. (Not a human, just a robot doing its lil thing. Respect.) πŸ€–


And that's a wrap on SoMField(3), bestie!! πŸ’œ This page came to you from Coin, Version 2.5.0, and yes, SoMField(3) is the whole entire moment. Study it, love it, go read the docs for real, and slay your multi-fields. Periodt. πŸ’…βœ¨πŸ¦‹

πŸ“¦ where this page comes from