Skip to content

Commit df434d6

Browse files
committed
allow local open and save
1 parent e1c5d24 commit df434d6

File tree

2 files changed

+158
-34
lines changed

2 files changed

+158
-34
lines changed

html/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,11 @@
134134
<button id="fit-all">Zoom Extents</button>
135135
<button id="solve">Solve</button>
136136
<button id="export-dxf" disabled>Download DXF</button>
137+
<button id="save-sketch">Save</button>
138+
<button id="open-sketch">Open</button>
137139
<button id="clear">Clear</button>
138140
</div>
141+
<input type="file" id="open-file-input" accept=".json" style="display:none">
139142
<div id="properties-panel"></div>
140143
<canvas id="solverCanvas"></canvas>
141144
<script type="module">

src/viewport.ts

Lines changed: 155 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { VerticalLine } from "./constraints/vertical_line.js";
88
import { HorizontalDistanceBetweenPoints } from "./constraints/horizontal_distance.js";
99
import { VerticalDistanceBetweenPoints } from "./constraints/vertical_distance.js";
1010
import { GradientBasedSolver } from "./gradient_based_solver.js";
11+
import { serializeSketchToString, deserializeSketchFromString } from "./sketch_serializer.js";
1112

1213
/** The tools available in the toolbar. */
1314
export type Tool = "none" | "point" | "line";
@@ -149,6 +150,25 @@ export class Viewport {
149150
document.getElementById("export-dxf")?.addEventListener("click", () => {
150151
this.exportDxf();
151152
});
153+
154+
// Save sketch button
155+
document.getElementById("save-sketch")?.addEventListener("click", () => {
156+
this.saveSketch();
157+
});
158+
159+
// Open sketch button + hidden file input
160+
const fileInput = document.getElementById("open-file-input") as HTMLInputElement | null;
161+
document.getElementById("open-sketch")?.addEventListener("click", () => {
162+
fileInput?.click();
163+
});
164+
fileInput?.addEventListener("change", () => {
165+
const file = fileInput.files?.[0];
166+
if (!file) return;
167+
file.text().then((text) => {
168+
this.openSketch(text);
169+
fileInput.value = ""; // allow re-opening the same file
170+
});
171+
});
152172
}
153173

154174
/** Adjust the viewport so all Point2 primitives are visible, with some padding. */
@@ -689,21 +709,51 @@ export class Viewport {
689709
info.style.marginBottom = "4px";
690710
panel.appendChild(info);
691711

712+
const dx = line.end.x - line.start.x;
713+
const dy = line.end.y - line.start.y;
714+
const length = Math.sqrt(dx * dx + dy * dy);
715+
const lengthDiv = document.createElement("div");
716+
lengthDiv.textContent = `Length: ${this.formatValue(length)}`;
717+
lengthDiv.style.marginBottom = "4px";
718+
panel.appendChild(lengthDiv);
719+
692720
// Check for existing HorizontalLine / VerticalLine constraint
693721
const constraints = this.sketch.getConstraintsOnPrimitive(line);
694722
const existingH = constraints.find((c): c is HorizontalLine => c instanceof HorizontalLine) ?? null;
695723
const existingV = constraints.find((c): c is VerticalLine => c instanceof VerticalLine) ?? null;
696724
const existingDirectional = existingH ?? existingV;
697725

698-
if (constraints.length > 0) {
726+
// Find distance constraints whose two points match the line's start/end (in either order)
727+
const matchesLineEndpoints = (c: HorizontalDistanceBetweenPoints | VerticalDistanceBetweenPoints): boolean => {
728+
const refs = c.getReferencedPrimitives();
729+
const [cp1, cp2] = refs;
730+
return (cp1 === line.start && cp2 === line.end) || (cp1 === line.end && cp2 === line.start);
731+
};
732+
733+
const allConstraints = this.sketch.getConstraints();
734+
const existingHDist = allConstraints.find(
735+
(c): c is HorizontalDistanceBetweenPoints =>
736+
c instanceof HorizontalDistanceBetweenPoints && matchesLineEndpoints(c)
737+
) ?? null;
738+
const existingVDist = allConstraints.find(
739+
(c): c is VerticalDistanceBetweenPoints =>
740+
c instanceof VerticalDistanceBetweenPoints && matchesLineEndpoints(c)
741+
) ?? null;
742+
743+
// Combine line-level and distance constraints for display
744+
const allDisplayed: import("./interfaces.js").ConstraintLike[] = [...constraints];
745+
if (existingHDist) allDisplayed.push(existingHDist);
746+
if (existingVDist) allDisplayed.push(existingVDist);
747+
748+
if (allDisplayed.length > 0) {
699749
const section = document.createElement("div");
700750
section.className = "prop-constraints";
701751
const heading = document.createElement("div");
702752
heading.className = "prop-subtitle";
703-
heading.textContent = `Constraints (${constraints.length})`;
753+
heading.textContent = `Constraints (${allDisplayed.length})`;
704754
section.appendChild(heading);
705755
const list = document.createElement("ul");
706-
for (const c of constraints) {
756+
for (const c of allDisplayed) {
707757
const li = document.createElement("li");
708758
li.textContent = c.description;
709759
list.appendChild(li);
@@ -712,48 +762,91 @@ export class Viewport {
712762
panel.appendChild(section);
713763
}
714764

715-
if (existingDirectional) {
716-
// Allow deleting the existing constraint
765+
// Delete buttons for existing constraints
766+
const deletable = [existingDirectional, existingHDist, existingVDist].filter(Boolean) as import("./interfaces.js").ConstraintLike[];
767+
if (deletable.length > 0) {
717768
const section = document.createElement("div");
718769
section.className = "prop-add-constraint";
719-
const deleteBtn = document.createElement("button");
720-
deleteBtn.textContent = `Delete ${existingDirectional.description}`;
721-
deleteBtn.addEventListener("click", () => {
722-
this.sketch.removeConstraint(existingDirectional);
723-
this.isSolved = false;
724-
this.updatePropertiesPanel();
725-
this.draw();
726-
});
727-
section.appendChild(deleteBtn);
770+
for (const c of deletable) {
771+
const deleteBtn = document.createElement("button");
772+
deleteBtn.textContent = `Delete ${c.description}`;
773+
deleteBtn.addEventListener("click", () => {
774+
this.sketch.removeConstraint(c);
775+
this.isSolved = false;
776+
this.updatePropertiesPanel();
777+
this.draw();
778+
});
779+
section.appendChild(deleteBtn);
780+
}
728781
panel.appendChild(section);
729-
} else {
730-
// Allow adding HorizontalLine or VerticalLine
782+
}
783+
784+
// Add constraint buttons
785+
const canAddH = !existingDirectional;
786+
const canAddV = !existingDirectional;
787+
const canAddHDist = !existingHDist;
788+
const canAddVDist = !existingVDist;
789+
790+
if (canAddH || canAddV || canAddHDist || canAddVDist) {
731791
const section = document.createElement("div");
732792
section.className = "prop-add-constraint";
733793
const heading = document.createElement("div");
734794
heading.className = "prop-subtitle";
735795
heading.textContent = "Add Constraint";
736796
section.appendChild(heading);
737797

738-
const addHBtn = document.createElement("button");
739-
addHBtn.textContent = "Add HorizontalLine";
740-
addHBtn.addEventListener("click", () => {
741-
this.sketch.addConstraint(new HorizontalLine(line));
742-
this.isSolved = false;
743-
this.updatePropertiesPanel();
744-
this.draw();
745-
});
746-
section.appendChild(addHBtn);
798+
if (canAddH) {
799+
const addHBtn = document.createElement("button");
800+
addHBtn.textContent = "Add HorizontalLine";
801+
addHBtn.addEventListener("click", () => {
802+
this.sketch.addConstraint(new HorizontalLine(line));
803+
this.isSolved = false;
804+
this.updatePropertiesPanel();
805+
this.draw();
806+
});
807+
section.appendChild(addHBtn);
808+
}
809+
810+
if (canAddV) {
811+
const addVBtn = document.createElement("button");
812+
addVBtn.textContent = "Add VerticalLine";
813+
addVBtn.addEventListener("click", () => {
814+
this.sketch.addConstraint(new VerticalLine(line));
815+
this.isSolved = false;
816+
this.updatePropertiesPanel();
817+
this.draw();
818+
});
819+
section.appendChild(addVBtn);
820+
}
821+
822+
if (canAddHDist) {
823+
const addHDistBtn = document.createElement("button");
824+
addHDistBtn.textContent = "Add Horizontal Distance";
825+
addHDistBtn.addEventListener("click", () => {
826+
const dist = parseFloat(prompt("Horizontal distance:", "1") ?? "");
827+
if (isNaN(dist)) return;
828+
this.sketch.addConstraint(new HorizontalDistanceBetweenPoints(line.start, line.end, dist));
829+
this.isSolved = false;
830+
this.updatePropertiesPanel();
831+
this.draw();
832+
});
833+
section.appendChild(addHDistBtn);
834+
}
835+
836+
if (canAddVDist) {
837+
const addVDistBtn = document.createElement("button");
838+
addVDistBtn.textContent = "Add Vertical Distance";
839+
addVDistBtn.addEventListener("click", () => {
840+
const dist = parseFloat(prompt("Vertical distance:", "1") ?? "");
841+
if (isNaN(dist)) return;
842+
this.sketch.addConstraint(new VerticalDistanceBetweenPoints(line.start, line.end, dist));
843+
this.isSolved = false;
844+
this.updatePropertiesPanel();
845+
this.draw();
846+
});
847+
section.appendChild(addVDistBtn);
848+
}
747849

748-
const addVBtn = document.createElement("button");
749-
addVBtn.textContent = "Add VerticalLine";
750-
addVBtn.addEventListener("click", () => {
751-
this.sketch.addConstraint(new VerticalLine(line));
752-
this.isSolved = false;
753-
this.updatePropertiesPanel();
754-
this.draw();
755-
});
756-
section.appendChild(addVBtn);
757850
panel.appendChild(section);
758851
}
759852

@@ -979,6 +1072,34 @@ export class Viewport {
9791072
URL.revokeObjectURL(url);
9801073
}
9811074

1075+
/** Serialize the current sketch to JSON and trigger a file download. */
1076+
private saveSketch(): void {
1077+
const json = serializeSketchToString(this.sketch);
1078+
const blob = new Blob([json], { type: "application/json" });
1079+
const url = URL.createObjectURL(blob);
1080+
const a = document.createElement("a");
1081+
a.href = url;
1082+
a.download = "sketch.json";
1083+
a.click();
1084+
URL.revokeObjectURL(url);
1085+
}
1086+
1087+
/** Load a sketch from a JSON string, replacing the current sketch. */
1088+
private openSketch(text: string): void {
1089+
try {
1090+
const restored = deserializeSketchFromString(text);
1091+
this.sketch = restored;
1092+
this.pendingLineStart = null;
1093+
this.pendingConstraint = null;
1094+
this.isSolved = false;
1095+
this.selectPrimitive(null);
1096+
this.zoomToFit();
1097+
this.showStatus("Sketch loaded");
1098+
} catch (e: any) {
1099+
this.showStatus(`Failed to open: ${e.message ?? e}`);
1100+
}
1101+
}
1102+
9821103
/** Draw a persistent solved/unsolved indicator in the top-right corner. */
9831104
private drawSolvedIndicator(ctx: CanvasRenderingContext2D): void {
9841105
const dpr = window.devicePixelRatio || 1;

0 commit comments

Comments
 (0)