- Added save file foramt
- Split main.py refactor
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user