Skip to content
Logo Theodo

Browzwear API Tips and Tricks

Robert Hofmann8 min read

Browzwear

VStitcher is a very powerful 3D apparel design software. It allows users to create realistic 3D renderings of their garments. In production contexts, you might want VStitcher to integrate more finely into your workflow and add features that are tailor-made to your company. That is where Browzwear plugins come in. With the Browzwear API, we can fill in the gaps left in VStitcher to create a more personalized set of functionality and reduce the amount of repeated work that designers need to perform.

But how do we create Browzwear plugins?

Outside of the official documentation, there is little information online about creating Browzwear plugins. If you want to do anything more complex than the example plugins that Browzwear provides, you are going to encounter some hiccups that need to be resolved. In the remainder of this article, I will be discussing a few of the unexpected ones that my team needed to do when developing more complex Browzwear plugin functionality.

Dealing with the popup from Adobe Illustrator whenever the plugin makes changes to an AI artwork.

If an artwork was uploaded as an Adobe Illustrator file, the Browzwear API uses Illustrator to make certain updates to that artwork. For example, whenever you manually change the color of an artwork that was originally an AI file, the change is done through Illustrator. One of the first issues we encountered when building a plugin for VStitcher is that when the Browzwear API opened Illustrator to make a change, a popup appeared in Adobe Illustrator asking the user for permission to run an external script.

AI popup when changing an artwork

This popup could appear a number of times while running a single command in the plugin, which practically eliminates any automation gains from having a plugin in the first place.

So how do we resolve this?

My colleague found the setting that needed to be disabled in Illustrator to prevent this popup and created a one-line Illustrator script.

app.preferences.setBooleanPreference("ShowExternalJSXWarning", false);

If a user runs this script once, they will not get the popup warning from Illustrator anymore. But it would be difficult to get all the future users of the plugin to run this Illustrator script manually, so what can you do? Have the script run automatically.

script_path = os.path.join(os.path.dirname(__file__), "scripts/security_fix.jsx")
app_path = "Adobe Illustrator"
subprocess.Popen(["open", "-a", app_path, script_path])

We added the above code to our BwApiPluginInit function, which automatically launches Illustrator on any Mac device and runs the script when our plugin is initialized. If a user does not have the popup already disabled, then they will get a single popup asking them to run the security script, and then they will not receive further popups with this warning from Illustrator.

Further Considerations

  1. The above trigger for the script is only designed for Mac. If you have users on Windows, you will need to first detect which operating system they are on, and then run the correct command for that operating system.
  2. The above script code (if run when the plugin is initialized), disables the popup until the setting is toggled again. If you have concerns about the security implications of this, you can instead have this script run when a user triggers a plugin command, and then when the command finishes running, you can run the opposite script (one that sets the warning back to true).

Waiting for VStitcher to finish doing something (like dressing a garment) before your command can continue.

I am a Typescript coder at heart. If there is a function that I need to wait for before I can continue running my code, my instinct is to use async/await. When building a plugin for Browzwear this is not feasible, as instead, Browzwear has a series of callbacks that will be triggered when certain actions are completed in VStitcher.

So how do we deal with this scenario in VStitcher?

The observer pattern. I personally have not thought about the observer pattern since my second year of university, but it is the perfect piece of the puzzle to use when writing a Browzwear plugin.

Let’s create a scenario where you will want to use the observer pattern. In this scenario, the user triggers one of the commands. This command creates a named snapshot of every pose of your garment on the current avatar, waiting for one to finish simulating before starting the simulation of the next pose.

To start, we need a publisher for when the garment simulation is finished.

class GarmentFinishedPublisher:

    _observers = []

    @staticmethod
    def notify(modifier=None):
        for observer in GarmentFinishedPublisher._observers:
            if modifier != observer:
                observer.update()

    @staticmethod
    def attach(observer):
				if observer not in GarmentFinishedPublisher._observers:
            GarmentFinishedPublisher._observers.append(observer)

    @staticmethod
    def detach(observer):
				try:
            GarmentFinishedPublisher._observers.remove(observer)
        except ValueError:
            pass

Then we need a trigger that will notify all of our observers whenever the garment’s simulation is finished.

In your [EventCallback.Run](http://EventCallback.Run) function, add the following:

if callback_id == GARMENT_SIMULATION_FINISHED_CALLBACK_ID:
		GarmentFinishedPublisher.notify()

In your BwApiPluginInit function, add the following:

BwApi.EventRegister(
		eventCallback, GARMENT_SIMULATION_FINISHED_CALLBACK_ID, BwApi.BW_API_EVENT_GARMENT_SIMULATION_FINISHED
)

Set GARMENT_SIMULATION_FINISHED_CALLBACK_ID to be a constant integer of any value that you desire (we went with 6), and now your publisher will notify any of its observers whenever the garment’s simulation is finished.

Now, we need to create an observer and add it to our command to complete our observer pattern.

Our observer class will store the list of snapshots we still need to take, and have an update function that includes a base case.

@dataclass
class GarmentFinishedObserver:
    garment_id: str
    snapshots_to_take: List

    def update(self):
        snapshot_name = self.snapshots_to_take[0]["pose_id"]
        BwApi.SnapshotSave(self.garment_id, snapshot_name)
        self.snapshots_to_take.pop(0)

        if len(self.snapshots_to_take) == 0:
            print("Finished Taking Snapshots")
            GarmentFinishedPublisher.detach(self)
            BwApi.GarmentUndress(self.garment_id)
        else:
						BwApi.AvatarPoseCurrentSet(self.snapshots_to_take[0]["pose_id"], 1, 0)
				    BwApi.GarmentDress(self.garment_id)

Then, when a user triggers our command, we need to populate the observer, add it to the publisher, and trigger the first pose to be simulated.

def take_snapshots(garment_id, data):
    print("Start of Take Snapshots")
    BwApi.GarmentUndress(garment_id)

    avatar_pose_ids = BwApi.AvatarCurrentPoseIds()

    snapshots_to_take = [
        ({"pose_id": pose_id})
        for pose_id in avatar_pose_ids
    ]
    BwApi.AvatarPoseCurrentSet(snapshots_to_take[0]["pose_id"], 1, 0)
    BwApi.GarmentDress(garment_id)

    garment_observer = GarmentFinishedObserver(garment_id, list(snapshots_to_take))
    GarmentFinishedPublisher.attach(garment_observer)

The poses will now be simulated one at a time, with the observer being called whenever one simulation is finished.

Updating 3D trims that are part of a smart trim.

The Browzwear API has a bug that prevents easy interaction with smart trims that have 3D trims (for example, the “puller” of a zipper). For most materials, you can get their information with BwApi.MaterialGet and update their information with BwApi.MaterialUpdate, but for 3D trims, this does not work and we need to take extra steps to handle them.

So how do we interact with 3D trims?

By exporting the u3ma for the material, editing it, and re-importing it. Let’s take it one step at a time.

First, getting the information for a 3D trim requires exporting the u3ma, unzipping it, finding the u3m file, and reading the u3m file.

def get_3D_trim(garment_id, colorway_id, material_id):
    with tempfile.TemporaryDirectory() as export_path:
        file_path = os.path.join(export_path, f"{material_id}.u3ma")
        BwApi.MaterialExport(
            garment_id,
            colorway_id,
            material_id,
            file_path,
        )

        with tempfile.TemporaryDirectory() as dir_path:
            zip_ref = zipfile.ZipFile(file_path, "r")
            zip_ref.extractall(dir_path)
            zip_ref.close()

            items_folder_path = [
                os.path.abspath(os.path.join(dir_path, name))
                for name in os.listdir(dir_path)
                if os.path.isdir(os.path.join(dir_path, name))
            ][0]

            for file in os.listdir(items_folder_path):
                if file.endswith(".u3m"):
                    u3m_path = os.path.join(items_folder_path, file)

            with open(u3m_path, "r", encoding="utf8") as u3m_file:
                material_json = json.loads(u3m_file.read())

            return {"material_json": material_json, "dir_path": dir_path, "u3m_path": u3m_path}

Now that we have the material_json, we can make changes to it as if it was any other material, but when it is time to push our updates to the material we need to go through a similar process as when getting the information.

To update a 3D trim, we need to update the u3m file, zip up our changes, and then import the new u3ma file.

def update_3D_trim(garment_id, colorway_id, material_id, material_json, dir_path, u3m_path):
    u3m_file = open(u3m_path, "w+", encoding="utf8")
    u3m_file.write(json.dumps(material_json, indent=4, sort_keys=True))
    u3m_file.close()

		shutil.make_archive(
        os.path.abspath(os.path.join(dir_path, f"{material_id}")),
        "zip",
        os.path.abspath(dir_path),
    )
    os.rename(
        os.path.abspath(os.path.join(dir_path, f"{material_id}.zip")),
        os.path.abspath(os.path.join(dir_path, f"{material_id}.u3ma")),
    )

    BwApi.MaterialUpdateFromFile(
        garment_id,
        colorway_id,
        material_id,
        os.path.abspath(os.path.join(dir_path, f"{material_id}.u3ma")),
    )

Further Considerations

  1. This process is far slower than directly getting and updating materials with the built-in Browzwear API commands. For now, this is the only way to handle updates to 3D trims, but once Browzwear updates its API to handle them more elegantly, any plugins that use this workaround should be updated. In addition, for any materials that are not 3D trims, the traditional BwApi.MaterialGet and BwApi.MaterialUpdate should be used.
  2. If your 3D trim is a nested 3D trim (where it is made up of more than one part), then instead of just finding the first u3m file and using that, you need to loop through the items_folder_path for every u3m file and make the update to all of them before re-zipping the 3D trim and calling BwApi.MaterialUpdateFromFile.

Where do we go from here?

The Browzwear API is a powerful assistant for creating plugins in VStitcher. Even though it is missing a few pieces of functionality, like the ability to easily update 3D trims or change the name of a color that you apply, the Browzwear API is worth taking the time to learn how to use. With it you can automate many of the tasks that your company’s VStitcher users do in their day-to-day, reducing time spent with busy-work and allowing each individual to make a greater impact.

And of course, if you need help building your plugin, you can always reach out to Theodo for assistance.

Liked this article?