# /// script # requires-python = ">=3.12" # # [tool.orcaslicer.plugin] # name = "Twistify" # description = "Twists, tapers, and wobbles every layer's slice polygons as a function of Z (demo)." # author = "SoftFever" # version = "0.03" # type = "slicing-pipeline" # /// """Twistify -- twist/taper/wobble any model at slice time. At Step.posSlice, every layer's sliced surfaces are transformed by a similarity about the object's bounding-box center as a function of Z -- edited IN PLACE through the host geometry classes (ExPolygon.rotate/scale/translate). Each surface is rotated about the center, then (if tapering) translated to the origin, uniformly scaled, and translated back, so the taper stays centered on the object instead of drifting toward the coordinate origin. An optional X wobble is applied last. After the per-region edits, layer.make_slices() re-derives the layer's merged islands so overhang/bridge/skirt/support stay coherent. The split slice loop runs make_perimeters() right after the hook, so the transform cascades into perimeters, infill, and the final G-code -- the preview corkscrews and the print keeps correct walls/infill/flow. Because we edit geometry in place, surface types are preserved automatically (no per-surface type carry needed), and no numpy is required -- rotate/scale/translate are host methods. Parameters come from self.get_config() (see _params below); the host seeds them from get_default_config() the first time the plugin loads. The first object layer is untouched (z_rel = 0), so bed adhesion is unaffected. The plugin also ships its own configuration UI (has_config_ui/get_config_ui): a self-contained page the Plugins dialog renders instead of the JSON editor, drawing the transform it is about to apply as an isometric stack of layer outlines. It reaches the host only through the injected window.orca bridge -- getConfig/saveConfig/restoreDefaults/getContext/onConfig/onTheme -- and styles itself from the --orca-* theme variables the host hands the frame, so it follows OrcaSlicer's light/dark theme. """ import math import json import orca _DEFAULTS = { "twist_deg_per_mm": 1.0, "taper_per_mm": 0.0, "wobble_ampl_mm": 0.0, "wobble_period_mm": 20.0, "min_scale": 0.05, } def _params(self): try: src = json.loads(self.get_config()) except (AttributeError, TypeError, ValueError): src = {} out = {} for key, default in _DEFAULTS.items(): try: out[key] = float(src[key]) except (KeyError, TypeError, ValueError): out[key] = default return out def _is_identity(p): return p["twist_deg_per_mm"] == 0.0 and p["taper_per_mm"] == 0.0 and p["wobble_ampl_mm"] == 0.0 def _layer_params(z_rel, mm_to_scaled, p): """(angle_rad, scale, x_offset_scaled) for one layer. Exact identity at z_rel == 0.""" theta = math.radians(p["twist_deg_per_mm"] * z_rel) s = max(p["min_scale"], 1.0 + p["taper_per_mm"] * z_rel) ox = 0.0 if p["wobble_ampl_mm"] != 0.0 and p["wobble_period_mm"] > 0.0: ox = p["wobble_ampl_mm"] * math.sin(2.0 * math.pi * z_rel / p["wobble_period_mm"]) * mm_to_scaled return theta, s, ox # The configuration page. Self-contained on purpose: it runs in an iframe sandboxed into an opaque # origin, so it has only the window.orca bridge and the --orca-* theme variables the host injects -- # no network, no same-origin access, no shared stylesheet. get_config_ui() substitutes _DEFAULTS for # __ORCA_DEFAULTS__, so Python owns the values and the page adds only presentation. _CONFIG_UI = """