+
+
+
+
+
+
+ Output Preview
+ No output generated yet.
+
+
+
+
+
+
+
+
diff --git a/web/js/app.js b/web/js/app.js
new file mode 100644
index 0000000..48988fb
--- /dev/null
+++ b/web/js/app.js
@@ -0,0 +1,217 @@
+import { calculateSummary, createModel, createThreadSizes } from "./calculator.js";
+import { buildXml, downloadTextFile } from "./xml.js";
+
+const form = document.querySelector("#config-form");
+const errorsEl = document.querySelector("#errors");
+const summaryEl = document.querySelector("#summary");
+const previewEl = document.querySelector("#xmlPreview");
+const downloadBtn = document.querySelector("#downloadBtn");
+const shareBtn = document.querySelector("#shareBtn");
+
+let generated = null;
+
+hydrateFormFromUrl();
+
+form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ const parseResult = parseConfig(form);
+
+ if (!parseResult.ok) {
+ renderErrors(parseResult.errors);
+ return;
+ }
+
+ const config = parseResult.config;
+ const structure = createModel(config);
+ const xml = buildXml(config, structure);
+ const summary = calculateSummary(structure);
+
+ generated = {
+ filename: config.filename,
+ xml,
+ summary,
+ firstDesignation: structure.model[0]?.designations[0]?.name ?? "n/a",
+ lastDesignation:
+ structure.model[structure.model.length - 1]?.designations[
+ structure.model[structure.model.length - 1]?.designations.length - 1
+ ]?.name ?? "n/a",
+ };
+
+ renderErrors([]);
+ renderOutput(generated);
+ writeUrlFromConfig(config);
+ downloadBtn.disabled = false;
+});
+
+downloadBtn.addEventListener("click", () => {
+ if (!generated) {
+ return;
+ }
+ downloadTextFile(generated.filename, generated.xml);
+});
+
+shareBtn.addEventListener("click", async () => {
+ const parseResult = parseConfig(form);
+ if (!parseResult.ok) {
+ renderErrors(parseResult.errors);
+ return;
+ }
+
+ writeUrlFromConfig(parseResult.config);
+ const shareUrl = window.location.href;
+
+ try {
+ await navigator.clipboard.writeText(shareUrl);
+ renderOutput(generated, "Share link copied to clipboard.");
+ } catch {
+ window.prompt("Copy this share URL:", shareUrl);
+ }
+});
+
+function parseConfig(formElement) {
+ const values = new FormData(formElement);
+ const errors = [];
+
+ const pitchStart = parseFloat(values.get("pitchStart"));
+ const pitchEnd = parseFloat(values.get("pitchEnd"));
+ const pitchStep = parseFloat(values.get("pitchStep"));
+ const sizeMin = parseInt(values.get("sizeMin"), 10);
+ const sizeMax = parseInt(values.get("sizeMax"), 10);
+ const threadAngle = parseFloat(values.get("angle"));
+ const threadForm = parseInt(values.get("threadForm"), 10);
+ const offsetsRaw = String(values.get("offsets") ?? "");
+
+ if (!(pitchStep > 0)) {
+ errors.push("Pitch step must be greater than 0.");
+ }
+
+ if (!(pitchEnd >= pitchStart)) {
+ errors.push("Pitch end must be greater than or equal to pitch start.");
+ }
+
+ if (!(sizeMin > 0 && sizeMax >= sizeMin)) {
+ errors.push("Diameter range is invalid. Ensure max >= min and both are positive.");
+ }
+
+ if (!(threadAngle > 0)) {
+ errors.push("Thread angle must be greater than 0.");
+ }
+
+ const toleranceOffsets = offsetsRaw
+ .split(",")
+ .map((part) => part.trim())
+ .filter(Boolean)
+ .map((part) => Number(part));
+
+ if (toleranceOffsets.length === 0 || toleranceOffsets.some((value) => Number.isNaN(value))) {
+ errors.push("Tolerance offsets must be comma-separated numeric values.");
+ }
+
+ if (errors.length > 0) {
+ return { ok: false, errors };
+ }
+
+ return {
+ ok: true,
+ config: {
+ threadName: String(values.get("threadName") ?? "3D-Printed Metric Threads V3"),
+ unit: String(values.get("unit") ?? "mm"),
+ filename: ensureXmlFilename(String(values.get("filename") ?? "3DPrintedMetricThreads.xml")),
+ threadAngle,
+ threadForm,
+ pitchStart,
+ pitchEnd,
+ pitchStep,
+ threadSizes: createThreadSizes(sizeMin, sizeMax),
+ toleranceOffsets,
+ },
+ };
+}
+
+function renderErrors(errors) {
+ errorsEl.innerHTML = "";
+ if (errors.length === 0) {
+ return;
+ }
+
+ for (const error of errors) {
+ const item = document.createElement("li");
+ item.textContent = error;
+ errorsEl.appendChild(item);
+ }
+
+ summaryEl.textContent = "Fix the validation errors to generate XML.";
+ summaryEl.className = "summary";
+ previewEl.textContent = "";
+ downloadBtn.disabled = true;
+}
+
+function renderOutput(result, notice = "") {
+ summaryEl.className = "summary ok";
+ const lines = [
+ `