- Added save file foramt

- Split main.py refactor
This commit is contained in:
bklronin
2026-07-05 22:16:08 +02:00
parent 3a169007f7
commit 5269c0897c
14 changed files with 9117 additions and 7371 deletions
+202
View File
@@ -1673,3 +1673,205 @@ class OCCSketch(SketchInterface):
return self._solver.dof() == 0
except Exception:
return False
# ─── Serialization (used by fluency.io.project_io) ─────────────────────
def to_dict(self) -> Dict[str, Any]:
"""Serialize the sketch to a plain-dict for JSON storage.
Captures: the workplane, every entity (with its current geometry and
flags), the constraint log, and the entity counter. Live solver
handles are intentionally NOT saved — the consumer must call
:meth:`from_dict` (or :meth:`rebuild_from_dict`) to rebuild the
SolveSpace system before solving again.
"""
# Sort entities by id so replay order is deterministic and matches
# creation order (ids are assigned monotonically by ``_next_id``).
entities_payload: List[Dict[str, Any]] = []
for eid in sorted(self._entities.keys()):
ent = self._entities[eid]
entities_payload.append(
{
"id": eid,
"type": ent.entity_type,
# geometry shape varies: point→(x,y), line→((x1,y1),(x2,y2)),
# circle→((cx,cy),r), arc→dict. All JSON-friendly.
"geometry": ent.geometry,
"is_construction": bool(ent.is_construction),
"is_external": bool(ent.is_external),
"constraints": list(ent.constraints),
}
)
# Sets become sorted lists for JSON. ``labels`` inside constraint_log
# is a set on the wire; convert to sorted list for JSON round-trip.
constraint_log_payload: List[Dict[str, Any]] = []
for entry in self._constraint_log:
constraint_log_payload.append(
{
"type": entry["type"],
"ids": list(entry["ids"]),
"params": list(entry["params"]),
"labels": sorted(entry["labels"]),
}
)
return {
"wp_origin": list(self._wp_origin),
"wp_normal": list(self._wp_normal),
"wp_x_dir": list(self._wp_x_dir),
"wp_y_dir": list(self._wp_y_dir),
"entity_counter": self._entity_counter,
"first_point_id": self._first_point_id,
"external_entity_ids": sorted(self._external_entity_ids),
"centerline_ids": sorted(self._centerline_ids),
"constraint_count": self._constraint_count,
"entities": entities_payload,
"constraint_log": constraint_log_payload,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "OCCSketch":
"""Build a fresh OCCSketch that reproduces the saved state.
Replays the construction sequence (points → lines → circles → arcs,
respecting external/centerline flags) and re-applies every constraint
in the saved log. The SolveSpace solver is deterministic for a given
input, so the post-solve state matches the saved one.
"""
sk = cls()
sk.rebuild_from_dict(data)
return sk
def rebuild_from_dict(self, data: Dict[str, Any]) -> None:
"""In-place restore from a dict produced by :meth:`to_dict`.
Wipes the current solver/state and re-creates every entity in id
order so :attr:`_first_point_id` is anchored correctly. Existing
callers (notably the solver-rebuild path on entity delete) don't use
this; only the project load path does.
"""
# Wipe solver + trackers (don't lose the workplane yet — we set it
# explicitly below).
self.clear()
self._external_entity_ids.clear()
self._centerline_ids.clear()
# 1. Workplane.
self.set_workplane(
tuple(data["wp_origin"]),
tuple(data["wp_normal"]),
tuple(data["wp_x_dir"]),
)
# 2. Force the entity counter so the replay assigns the same ids as
# the saved sketch — the constraint log references those ids.
self._entity_counter = int(data.get("entity_counter", 0))
# 3. Replay entities in id order. We need the OCCSketchEntity
# objects back (for arc center/start/end lookups), so we
# reconstruct by id and let ``_next_id`` advance the counter.
entities_by_id: Dict[int, OCCSketchEntity] = {}
for entry in data.get("entities", []):
eid = int(entry["id"])
# Ensure the next _next_id() call returns eid.
self._entity_counter = eid - 1
etype = entry["type"]
geom = entry.get("geometry")
is_external = bool(entry.get("is_external", False))
if etype == "point":
x, y = float(geom[0]), float(geom[1])
if is_external:
ent = self.add_external_point(x, y)
else:
ent = self.add_point(x, y)
elif etype == "line":
# line geometry is ((x1,y1),(x2,y2)); we already know the
# endpoints exist as point entities. Look them up by saved
# position via _points (which was just populated above).
(x1, y1), (x2, y2) = geom
s_id = self._find_point_at(x1, y1)
e_id = self._find_point_at(x2, y2)
if s_id is None or e_id is None:
logger.warning(
"Skipping line %s during load: endpoints not found", eid
)
continue
if is_external:
ent = self.add_external_line(entities_by_id[s_id], entities_by_id[e_id])
else:
ent = self.add_line(entities_by_id[s_id], entities_by_id[e_id])
elif etype == "circle":
(cx, cy), radius = geom
c_id = self._find_point_at(cx, cy)
if c_id is None:
logger.warning(
"Skipping circle %s during load: center not found", eid
)
continue
ent = self.add_circle(entities_by_id[c_id], float(radius))
elif etype == "arc":
center_pos = tuple(geom["center"])
start_pos = tuple(geom["start"])
end_pos = tuple(geom["end"])
radius = float(geom["radius"])
sweep = float(geom.get("sweep", 0.0))
c_id = self._find_point_at(*center_pos)
s_id = self._find_point_at(*start_pos)
e_id = self._find_point_at(*end_pos)
if c_id is None or s_id is None or e_id is None:
logger.warning(
"Skipping arc %s during load: endpoints not found", eid
)
continue
ent = self.add_arc(
entities_by_id[c_id],
radius,
entities_by_id[s_id],
entities_by_id[e_id],
sweep=sweep,
)
else:
logger.warning("Unknown sketch entity type %r; skipping", etype)
continue
# Restore the per-entity UI flags / labels that aren't carried
# by the add_* methods themselves.
ent.is_construction = bool(entry.get("is_construction", False))
ent.constraints = list(entry.get("constraints", []))
entities_by_id[eid] = ent
# 4. Replay constraint log. ``_apply_constraint_log`` re-issues the
# solver call and pushes back into the entity tracker via
# ``entity.constraints``. We don't double-record into the log
# itself (the log was already cleared by ``clear()`` and we
# re-populate it here, so the live ``_constraint_count`` will
# reflect the saved state at the end).
for entry in data.get("constraint_log", []):
self._record_constraint(
entry["type"],
tuple(entry["ids"]),
tuple(entry.get("params", ())),
tuple(entry.get("labels", ())),
)
# Re-apply the live solver calls AFTER the log is restored, so that
# the constraint tracker matches the solver state on a fresh solve.
for entry in self._constraint_log:
self._apply_constraint_log(entry)
# 5. Restore external / centerline id sets. ``add_external_*`` adds
# to the set internally; if the entity's id was a regular entity
# for some reason (legacy / hand-edited file), fold it in too so
# the saved flag is authoritative.
for eid in data.get("external_entity_ids", []):
self._external_entity_ids.add(int(eid))
for eid in data.get("centerline_ids", []):
self._centerline_ids.add(int(eid))
def _find_point_at(self, x: float, y: float, tol: float = 1e-6) -> Optional[int]:
"""Return the entity id of a point sitting at UV ``(x, y)`` (within tol)."""
for pid, pos in self._points.items():
if abs(pos[0] - x) < tol and abs(pos[1] - y) < tol:
return pid
return None