diff --git a/scripts/developer/accelerationFilterApp.js b/script-archive/accelerationFilterApp.js
similarity index 99%
rename from scripts/developer/accelerationFilterApp.js
rename to script-archive/accelerationFilterApp.js
index a05e7bacf0..7bde13ad28 100644
--- a/scripts/developer/accelerationFilterApp.js
+++ b/script-archive/accelerationFilterApp.js
@@ -219,4 +219,3 @@ Script.scriptEnding.connect(function() {
}
tablet.removeButton(tabletButton);
});
-
diff --git a/scripts/developer/exponentialFilterApp.js b/script-archive/exponentialFilterApp.js
similarity index 99%
rename from scripts/developer/exponentialFilterApp.js
rename to script-archive/exponentialFilterApp.js
index 58c0e49e31..86b4f3032d 100644
--- a/scripts/developer/exponentialFilterApp.js
+++ b/script-archive/exponentialFilterApp.js
@@ -237,4 +237,3 @@ Script.scriptEnding.connect(function() {
}
tablet.removeButton(tabletButton);
});
-
diff --git a/scripts/developer/characterSmoothing/characterSmoothing.js b/scripts/developer/characterSmoothing/characterSmoothing.js
new file mode 100644
index 0000000000..c2321b9275
--- /dev/null
+++ b/scripts/developer/characterSmoothing/characterSmoothing.js
@@ -0,0 +1,115 @@
+//
+// Copyright 2023 Overte e.V.
+
+let smoothing_settings = {
+ enabled: true,
+ targets: {
+ LeftHand: { transform: 1, rotation: 1 },
+ RightHand: { transform: 1, rotation: 1 },
+ LeftFoot: { transform: 1, rotation: 1 },
+ RightFoot: { transform: 1, rotation: 1 },
+ Hips: { transform: 1, rotation: 1 },
+ Spine2: { transform: 1, rotation: 1 },
+ },
+};
+
+let mapping;
+let mapping_settings = {
+ name: "org.overte.controllers.smoothing",
+ channels: [],
+};
+
+const html_url = Script.resolvePath("./ui/index.html");
+let tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
+let shown = false;
+
+tablet.screenChanged.connect(onScreenChanged);
+Script.scriptEnding.connect(shutdownTabletApp);
+
+let tabletButton = tablet.addButton({
+ text: "AviSmooth",
+ icon: Script.resolvePath("./img/icon.png"),
+ activeIcon: Script.resolvePath("./img/icon-a.png"),
+});
+
+tabletButton.clicked.connect(() => {
+ tablet.gotoWebScreen(html_url);
+});
+
+function onScreenChanged(type, url) {
+ if (type !== "Web" || url !== html_url) {
+ tablet.webEventReceived.disconnect(onWebEventReceived);
+ tabletButton.editProperties({ isActive: false });
+ shown = false;
+ } else {
+ tabletButton.editProperties({ isActive: true });
+ tablet.webEventReceived.connect(onWebEventReceived);
+ shown = true;
+ smoothing_settings = Settings.getValue(
+ "smoothing_settings",
+ smoothing_settings
+ );
+ }
+}
+
+function shutdownTabletApp() {
+ if (mapping) mapping.disable(); // Disable custom mapping
+ tablet.removeButton(tabletButton); // Remove the app
+ tablet.screenChanged.disconnect(onScreenChanged);
+ tablet.webEventReceived.disconnect(onWebEventReceived);
+}
+
+const _sendMessage = (message) =>
+ tablet.emitScriptEvent(JSON.stringify(message));
+
+function onWebEventReceived(message) {
+ message = JSON.parse(message);
+
+ if (message.action === "ready") {
+ _sendMessage({
+ action: "initialize",
+ data: smoothing_settings,
+ });
+ }
+
+ if (message.action === "new_settings") {
+ smoothing_settings = message.data;
+ mappingChanged();
+ }
+
+ if (message.action === "set_state") {
+ smoothing_settings.enabled = message.data;
+ mappingChanged();
+ }
+}
+
+function mappingChanged() {
+ Settings.setValue("smoothing_settings", smoothing_settings);
+ if (mapping) mapping.disable();
+
+ if (smoothing_settings.enabled) {
+ // Build mapping_settings
+ mapping_settings.channels = [];
+
+ Object.keys(smoothing_settings.targets).forEach((target) =>
+ mapping_settings.channels.push(_generateChannel(target))
+ );
+
+ function _generateChannel(name) {
+ return {
+ from: `Standard.${name}`,
+ to: `Actions.${name}`,
+ filters: [
+ {
+ type: "exponentialSmoothing",
+ translation: smoothing_settings.targets[name].transform,
+ rotation: smoothing_settings.targets[name].rotation,
+ },
+ ],
+ };
+ }
+
+ mapping = Controller.parseMapping(JSON.stringify(mapping_settings));
+ mapping.enable();
+ }
+}
diff --git a/scripts/developer/characterSmoothing/img/icon-a.png b/scripts/developer/characterSmoothing/img/icon-a.png
new file mode 100644
index 0000000000..aa603c2a45
Binary files /dev/null and b/scripts/developer/characterSmoothing/img/icon-a.png differ
diff --git a/scripts/developer/characterSmoothing/img/icon.png b/scripts/developer/characterSmoothing/img/icon.png
new file mode 100644
index 0000000000..ac25aeb647
Binary files /dev/null and b/scripts/developer/characterSmoothing/img/icon.png differ
diff --git a/scripts/developer/characterSmoothing/ui/index.css b/scripts/developer/characterSmoothing/ui/index.css
new file mode 100644
index 0000000000..641d00c6a8
--- /dev/null
+++ b/scripts/developer/characterSmoothing/ui/index.css
@@ -0,0 +1,39 @@
+body {
+ font-family: Verdana, Geneva, Tahoma, sans-serif;
+}
+
+.container {
+ margin-bottom: 10px;
+}
+.container .content .type {
+ display: grid;
+ grid-template-columns: 1fr 3fr 1fr;
+ grid-template-rows: 1fr 1fr;
+ width: 100%;
+}
+.container .content .type span {
+ margin: 0 10px;
+}
+.container .content .type .header {
+ grid-column-start: 1;
+ grid-column-end: 3;
+}
+.container .content .type .type-value {
+ text-align: center;
+}
+.container .content .type input {
+ width: 100%;
+ grid-column-start: 1;
+ grid-column-end: 3;
+}
+
+.container:last-child {
+ margin-bottom: 0;
+}
+
+.horizontal-button-container {
+ margin-top: 10px;
+}
+.horizontal-button-container button:focus {
+ outline: 0;
+}
\ No newline at end of file
diff --git a/scripts/developer/characterSmoothing/ui/index.html b/scripts/developer/characterSmoothing/ui/index.html
new file mode 100644
index 0000000000..6d745b177a
--- /dev/null
+++ b/scripts/developer/characterSmoothing/ui/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/developer/characterSmoothing/ui/index.js b/scripts/developer/characterSmoothing/ui/index.js
new file mode 100644
index 0000000000..87010164bf
--- /dev/null
+++ b/scripts/developer/characterSmoothing/ui/index.js
@@ -0,0 +1,107 @@
+// Helper functions
+const qs = (target) => document.querySelector(target);
+const qsa = (target) => document.querySelectorAll(target);
+
+// Message listeners
+const _sendMessage = (message) =>
+ EventBridge.emitWebEvent(JSON.stringify(message));
+EventBridge.scriptEventReceived.connect((message) =>
+ newMessage(JSON.parse(message))
+);
+
+// Settings & data
+let smoothing_settings = {};
+
+function newMessage(message) {
+ if (message.action === "initialize") {
+ initialize(message.data);
+ }
+}
+
+function initialize(data) {
+ smoothing_settings = data;
+
+ // Clear all existing listings (if any)
+ qsa("body .target-list").forEach((item) => item.remove());
+
+ // Set state
+ if (smoothing_settings.enabled === false) _toggleEnabledFalse();
+ if (smoothing_settings.enabled === true) _toggleEnabledTrue();
+
+ // For each target point
+ Object.keys(smoothing_settings.targets).forEach((target) => {
+ // Use the target data to build a listing
+ let template = qs("#target-template").content.cloneNode(true);
+
+ template.querySelector(".container").dataset.name = target;
+ template.querySelector(".container-header").innerText = target;
+
+ const rotation_area = template.querySelector('[data-value="rotation"]');
+ const transform_area = template.querySelector(
+ '[data-value="transform"]'
+ );
+
+ rotation_area.querySelector("input").value = _fromDecimal(
+ smoothing_settings.targets[target].rotation
+ );
+ transform_area.querySelector("input").value = _fromDecimal(
+ smoothing_settings.targets[target].transform
+ );
+
+ rotation_area.querySelector(".type-value").innerText = _formatPercent(
+ _fromDecimal(smoothing_settings.targets[target].rotation)
+ );
+ transform_area.querySelector(".type-value").innerText = _formatPercent(
+ _fromDecimal(smoothing_settings.targets[target].transform)
+ );
+
+ rotation_area.querySelector("input").addEventListener("change", () => {
+ rotation_area.querySelector(".type-value").innerText =
+ _formatPercent(rotation_area.querySelector("input").value);
+ smoothing_settings.targets[target].rotation = _toDecimal(
+ rotation_area.querySelector("input").value
+ );
+ });
+ transform_area.querySelector("input").addEventListener("change", () => {
+ transform_area.querySelector(".type-value").innerText =
+ _formatPercent(transform_area.querySelector("input").value);
+ smoothing_settings.targets[target].transform = _toDecimal(
+ transform_area.querySelector("input").value
+ );
+ });
+
+ // // Append our newly created child
+ qs("#target-list").appendChild(template);
+ });
+}
+
+qsa("input").forEach((button) =>
+ button.addEventListener("click", (event) => event.target.blur())
+);
+
+function toggleSmoothing() {
+ if (smoothing_settings.enabled) _toggleEnabledFalse();
+ else _toggleEnabledTrue();
+}
+
+function _toggleEnabledFalse() {
+ _sendMessage({ action: "set_state", data: false });
+ qs("#toggle-button").classList.remove("bad");
+ qs("#toggle-button").classList.add("good");
+ qs("#toggle-button").innerText = "Enable";
+ smoothing_settings.enabled = false;
+}
+function _toggleEnabledTrue() {
+ _sendMessage({ action: "set_state", data: true });
+ qs("#toggle-button").classList.remove("good");
+ qs("#toggle-button").classList.add("bad");
+ qs("#toggle-button").innerText = "Disable";
+ smoothing_settings.enabled = true;
+}
+
+_sendMessage({ action: "ready" });
+const applySettings = () =>
+ _sendMessage({ action: "new_settings", data: smoothing_settings });
+const _formatPercent = (value) => parseInt(value).toString() + " %";
+const _toDecimal = (value) => value / 100;
+const _fromDecimal = (value) => value * 100;
diff --git a/scripts/developer/characterSmoothing/ui/index.scss b/scripts/developer/characterSmoothing/ui/index.scss
new file mode 100644
index 0000000000..acfd97f7a5
--- /dev/null
+++ b/scripts/developer/characterSmoothing/ui/index.scss
@@ -0,0 +1,47 @@
+body {
+ font-family: Verdana, Geneva, Tahoma, sans-serif;
+}
+
+.container {
+ margin-bottom: 10px;
+
+ .content {
+ .type {
+ display: grid;
+ grid-template-columns: 1fr 3fr 1fr;
+ grid-template-rows: 1fr 1fr;
+ width: 100%;
+
+ span {
+ margin: 0 10px;
+ }
+
+ .header {
+ grid-column-start: 1;
+ grid-column-end: 3;
+ }
+
+ .type-value {
+ text-align: center;
+ }
+
+ input {
+ width: 100%;
+ grid-column-start: 1;
+ grid-column-end: 3;
+ }
+ }
+ }
+}
+
+.container:last-child {
+ margin-bottom: 0;
+}
+
+.horizontal-button-container {
+ margin-top: 10px;
+
+ button:focus{
+ outline: 0;
+ }
+}
diff --git a/scripts/developer/characterSmoothing/ui/theme.css b/scripts/developer/characterSmoothing/ui/theme.css
new file mode 100644
index 0000000000..f45b4bdf8b
--- /dev/null
+++ b/scripts/developer/characterSmoothing/ui/theme.css
@@ -0,0 +1,75 @@
+body {
+ margin: 2px;
+ box-sizing: border-box;
+ background-color: #101010;
+ font-size: 16px;
+ color: white;
+}
+
+.color-primary {
+ background-color: #212121;
+}
+
+.color-secondary {
+ background-color: #333333;
+}
+
+.color-tertiary {
+ background-color: #454545;
+}
+
+.container {
+ width: 100%;
+ min-height: 2px;
+}
+.container .container-header {
+ font-size: 18px;
+ padding: 0.5rem 1rem;
+ box-sizing: border-box;
+ margin-bottom: 5px;
+}
+.container .content {
+ padding: 0 10px;
+}
+
+.horizontal-button-container {
+ display: flex;
+ flex-direction: row;
+}
+.horizontal-button-container button {
+ width: 100%;
+ margin: auto;
+ margin-right: 10px;
+}
+.horizontal-button-container button:last-child {
+ margin-right: 0;
+}
+
+button {
+ border: 0;
+ padding: 1rem;
+ box-sizing: border-box;
+ background-color: #696e6f;
+ color: white;
+ cursor: pointer;
+}
+
+button:hover {
+ filter: brightness(70%);
+}
+
+button:active {
+ filter: brightness(50%);
+}
+
+button.good {
+ background-color: #077e30;
+}
+
+button.bad {
+ background-color: #771d1d;
+}
+
+button.generic {
+ background-color: #505186;
+}
\ No newline at end of file