Skip to content

Releases: medinstech/cycloidgen

cycloidgen v7.8.0

Choose a tag to compare

@github-actions github-actions released this 28 Aug 15:04

Numbers — none move. Nothing here touches the geometry, the analysis or what
an export contains. A design opened in 7.8.0 analyses exactly as it did in
7.7.0; what is new is that the application can now tell you when it is no longer
the version those numbers should be coming from.

Added

  • It can say when it is out of date, if you say it may. An engineering tool
    is installed once and used for months, and this one has had no way to mention
    that it had fallen behind. That is not a missing convenience. The models
    change between releases — the changelog above is mostly a record of numbers
    moving — so a design sized against a build from two versions ago can disagree
    with the current one about the same drive, and the person reading the old
    answer has no reason to suspect it. There was nothing in the application that
    could tell them.

    Help ▸ Check for updates… asks GitHub which release is the newest and says
    so either way. Help ▸ Check for updates automatically does the same once a
    day, in the background, and stays quiet unless there is something to say.

    Nothing is sent before you are asked. The first run puts the question up
    once, in plain words, and until it is answered no request is made by any path.
    What a check sends is a request for a version number: nothing about your
    designs, your machine or your files, and there is nothing in the application
    that collects any of that in the first place.

    A "no" is remembered, and there are three different ones. Not now asks again
    tomorrow. Skip this version means that particular release is not wanted and
    nothing more is said about it until a newer one appears — asking from the menu
    overrides it, because that is the user putting the question again. Turning the
    setting off stops it entirely. A prompt that cannot be dismissed for good is a
    prompt people learn to click through, and then the one that matters goes the
    same way.

    The dialog carries the release notes rather than only the version number. "Is
    7.8.1 worth installing in the middle of a job" is a question about what
    changed, and sending the reader to a browser to find out is how it gets
    postponed indefinitely.

  • On Windows, it can do the install. Choosing Download and install fetches
    the installer with a progress bar you can cancel, checks it against the length
    GitHub published for it, closes the application and starts it. Windows asks for
    permission first — the installer needs it, and that decision stays with the
    person in front of the machine. Preferences, recent files and the restored
    session survive it; the installer clears the old bundle rather than copying
    over it, which is why the application closes first.

    The build is still unsigned, and the dialog says so rather than implying a
    verification that has not happened. SmartScreen warns exactly as it does for an
    installer fetched in a browser, and the download is checked for being whole,
    not for being ours. A signed build is still planned.

  • What it offers depends on how this copy was installed, because offering the
    wrong route is worse than offering none. A pip install is given the upgrade
    command with the interpreter that is actually running named in it — a machine
    with several Pythons is exactly where a bare pip install -U upgrades a copy
    that is not the one in front of you. An AppImage or a disk image is a file you
    put somewhere yourself and is not ours to replace, so those are sent to the
    release page. Only the Windows installer, and only on the architecture the
    release actually carries a bundle for, is offered as an install.

Changed

  • certifi is a dependency now. ssl.create_default_context() reads the
    platform certificate store, which is the right answer on Windows and on a
    normal Linux install and the wrong one inside a frozen bundle on macOS: OpenSSL
    looks in the paths it was built against, they belong to the CI runner, and a
    perfectly good connection fails verification. It is imported optionally, so a
    copy installed without it falls back to the platform store rather than
    failing — and nothing anywhere disables verification.

  • CYCLOIDGEN_NO_UPDATE_CHECK turns the whole thing off, by every path including
    the menu, which stays visible and greyed out saying why. It exists for the same
    reason CYCLOIDGEN_SETTINGS does: whoever ships a copy may have a better
    answer than we do about how it gets updated, and a distribution whose users
    update through their package manager should not be second-guessed by the
    application.


Windows - cycloidgen_..._Setup.exe. It is unsigned, so
SmartScreen will warn on first run: More info -> Run anyway. A
signed build is planned.

Linux - cycloidgen-...-x86_64.AppImage. chmod +x and run it;
it needs glibc 2.35 or newer. If it will not start, the machine has no
FUSE 2: install libfuse2, or run the file with
--appimage-extract-and-run, which unpacks it instead of mounting it.

macOS - cycloidgen-...-arm64.dmg, Apple silicon. Drag it to
Applications, then run this before opening it:

xattr -dr com.apple.quarantine /Applications/cycloidgen.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first
launch. On macOS 15 and later the dialog it raises defaults to
Move to Trash, which deletes the install - there is no Open
Anyway
in that dialog, it appears in System Settings only after a
launch has been blocked, and right-click -> Open no longer bypasses
it. Clearing the flag first avoids the dialog entirely. On an Intel
Mac, use pip.

Anywhere with Python 3.10-3.14: pip install cycloidgen, then
cycloidgen.

The numbers this produces are preliminary sizing estimates, not a
certification. Validate against a physical prototype before anything
load-bearing depends on them.

cycloidgen v7.7.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 12:59

Numbers — none move. A design with no duty cycle analyses exactly as it did:
the field defaults to empty, nothing about a cycle feeds back into the geometry
or the rated point, and tests/test_duty.py holds that by comparing whole report
sections between a design with a cycle and the same design without one. What is
new is a set of numbers a single rated point could not produce.

Added

  • A duty cycle, because a machine is not one point. The app has always taken
    the duty as a torque out at a speed in, which is the right way to size a
    gearbox and the wrong way to describe a machine. A robot joint lifts, holds,
    returns and waits; a winch pulls hard and slowly and spools back fast and
    empty. A single point can be the worst of those or the average of them, and
    the app has been asking which one you meant without saying that it was asking.

    Design ▸ Duty cycle (Ctrl+D) takes a table of them: what it does, the
    torque, the speed at the output, and how long it lasts. The durations are in
    whatever unit suits — only the ratios are read — so a cycle can be typed the
    way it was measured rather than converted into shares that have to add up to
    one.

    The point of it is that the quantities do not aggregate the same way, and
    no single point is conservative for all of them at once:

    • Stress takes the worst point. Averaging the hardest moment away is how a
      drive passes on paper and cracks on a bench.
    • Temperature takes the mean loss. A housing integrates, and sizing the
      cooling to the peak is sizing it to a transient.
    • Bearing life takes neither. Life goes as the cube of load, so a varying
      load is carried at ISO 281's equivalent load — the cubic mean, which sits
      well above the arithmetic mean and well below the peak.
    • The motor takes both ends: it has to make the peak and survive the RMS.

    Four aggregations from one cycle. Getting any of them by picking a
    representative point is a coincidence rather than a method.

  • Holding still is a point, not an error. Zero output speed is most of what
    some drives do: the torque is there so the contact loads are, nothing slides
    so there is no PV and no friction loss, nothing turns so no bearing life is
    consumed. It falls out of the arithmetic rather than being special-cased —
    the cubic mean weighs revolutions, so a point at half the speed contributes
    half the wear and a point at no speed contributes none, and the equivalent
    speed is averaged over the whole cycle including the standstill. That last
    one is what makes the life a number of hours of cycle rather than hours of
    rotation, which is the unit a service interval is actually written in.

  • DUTY_RATING_MISMATCH, and it is the check that makes the rest safe to
    read.
    Everything on the datasheet — capacity, safety factor, wind-up,
    transmission error, fatigue — is computed at the rated torque. State a cycle
    that goes above it and every one of those numbers is describing an easier
    machine than the one you have just described. The drive is not wrong; the page
    in front of you is. Beside it, DUTY_MOTOR_SHORT names the hardest moment
    which is not always the heaviest point, because torque falls with speed and a
    light point at speed can be tighter than a heavy one standing still — and
    DUTY_BEARING_LIFE reports the drive's own bearings over the cycle.

    That last one is the one that had to be built twice. The first version
    re-selected bearings for the cycle, which answers "what would I fit for this
    duty" and can therefore never report a bearing that falls short: it would
    simply have picked a bigger one. The question worth asking is whether the
    parts the drive has survive what it will actually do, so the rated
    schedule's parts are kept and only their lives are recomputed — at one scalar
    equivalent torque and one equivalent speed, which is exact here because every
    load in this machine is linear in output torque and every speed is linear in
    input speed. Doing it that way also means nothing has to match a role by its
    name to know what a number means, which is how the bearing quantities went
    wrong once before.

  • Where it shows up. A section on the datasheet and in the PDF, a duty
    block in the JSON with every point and every aggregate, and a shape that
    matches the motor block: the key is always there so a consumer can tell "no
    cycle stated" from "an older version that could not be asked", with nulls
    rather than zeros so nothing reads a missing answer as a good one.

Changed

  • analysis.bearings.life_hours is public. It is a fact about a part rather
    than a step in selecting one, and the duty cycle asks it of parts that have
    already been chosen.

Windows - cycloidgen_..._Setup.exe. It is unsigned, so
SmartScreen will warn on first run: More info -> Run anyway. A
signed build is planned.

Linux - cycloidgen-...-x86_64.AppImage. chmod +x and run it;
it needs glibc 2.35 or newer. If it will not start, the machine has no
FUSE 2: install libfuse2, or run the file with
--appimage-extract-and-run, which unpacks it instead of mounting it.

macOS - cycloidgen-...-arm64.dmg, Apple silicon. Drag it to
Applications, then run this before opening it:

xattr -dr com.apple.quarantine /Applications/cycloidgen.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first
launch. On macOS 15 and later the dialog it raises defaults to
Move to Trash, which deletes the install - there is no Open
Anyway
in that dialog, it appears in System Settings only after a
launch has been blocked, and right-click -> Open no longer bypasses
it. Clearing the flag first avoids the dialog entirely. On an Intel
Mac, use pip.

Anywhere with Python 3.10-3.14: pip install cycloidgen, then
cycloidgen.

The numbers this produces are preliminary sizing estimates, not a
certification. Validate against a physical prototype before anything
load-bearing depends on them.

cycloidgen v7.6.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 09:32

Numbers — none move. Every existing design analyses exactly as it did:
motor_kind defaults to none, nothing about the motor feeds back into the
geometry, and tests/test_motor.py holds that promise by comparing whole report
sections between a design with a motor stated and the same design without one.
What is new is a set of numbers that were not there before.

Added

  • The motor is a curve now, not a mounting face. The app has always known
    which NEMA frame a drive bolts to and nothing whatever about what comes out of
    the shaft — so the duty point was taken on trust: an output torque at a speed,
    worked back through the ratio and the efficiency to an input torque nobody
    then asked about. That is a complete answer to is the gearbox strong enough
    and no answer at all to will this turn, which is the question a drive is
    bought to settle. Set a torque curve under Motor in the panel and the
    analysis puts the duty point on it: what the motor has there, what is being
    asked of it, and the margin between.

    Two models, both closed form, both stated with their limits in
    core/motor.py.
    A stepper's torque follows its phase current, and the current is what the
    supply can force through the winding against its resistance, its inductive
    reactance and its own back-EMF — V^2 = (K*w_m + I*R)^2 + (w_e*L*I)^2, solved
    for I and capped at the rated current. A DC or brushless motor gets the
    straight line between Kt*V/R at stall and Kv*V at no load, with the
    continuous rating carried as a separate, much lower line.

    Eight numbers off a datasheet, and three of them are shared between the two
    kinds because the sharing is physical rather than a squeeze: both motors run
    off the same bus, both datasheets print a winding resistance, and both have a
    current they will hold all day. What is not shared is what that current
    means — a stepper is current limited everywhere, so its curve is already its
    continuous curve, and a brushless motor's is drawn at stall current with a
    continuous line two orders of magnitude below it. Sizing a gearbox on the
    first is the classic way to build one that survives the bench and not the
    robot, so the margin reported here is on the continuous line and the peak is
    carried alongside rather than instead.

    The identity that makes this cheap is worth stating: torque per amp and volts
    per radian per second are the same constant, so a stepper's electrical
    ceiling falls out of its mechanical rating with nothing else needed —
    n_ceiling = 60*V/(2*pi*K), where K = T_hold/(sqrt(2)*I_rated) because
    holding torque is measured with both phases energised. On 24 V a 0.4 Nm,
    1.7 A motor makes no torque at all past about 1380 rpm; on 12 V the same motor
    stops at 690. Halving the supply halves the top speed, and the supply is
    usually the cheapest thing in the design to change.

    It also says something people do not expect: on a fixed bus and a fixed
    current, more holding torque buys less top speed. More torque per amp is
    more volts per rad/s, so the bigger motor meets the supply sooner. Buying
    torque without buying current or volts moves the ceiling down.

  • Three checks and a reading. MOTOR_OPERATING_POINT says where on the
    curve this duty point sits — how much of the standstill torque is left here
    and how close to the ceiling it is, which is the number that catches a drive
    designed on holding torque. MOTOR_TORQUE_SHORT is the test — thin
    margin, a duty that is a burst rather than a rating, or a motor that will not
    turn the drive at all. A warning in every case, including the last: an error
    in this app means the files are wrong, and these files are right. What is
    wrong is which motor goes on the end, which is fixed by buying a different one
    as readily as by redrawing anything — the same argument MOTOR_SHAFT_MISMATCH
    has always been a warning on. Blocking the export would also have made the app
    refuse to hand over a gearbox somebody is having machined so they can go and
    find a motor for it. MOTOR_SUPPLY_VOLTAGE catches the
    one thing the winding resistance decides on its own: a bus that cannot push
    the rated current through a stationary winding, which scales the whole curve
    down from the datasheet's, standing still included.

  • A MOTOR tab, the curve with the duty point on it and the crossing where
    the motor runs out labelled, plus seven rows on the datasheet and a section in
    the PDF. Two of those rows are the ones to design against: the output torque
    this motor buys
    through this reduction at this efficiency, and the output
    speed
    past which it cannot make the required torque. On most designs here
    the first is smaller than the gearbox's own capacity — so the motor, not the
    contact stress, is what the drive is worth, and that comparison is now on one
    page.

  • The reduction can come from the motor. In the search dialog, work it out
    from the motor
    : state what the output has to do and the reduction stops
    being part of the question. design.ratio_band asks the curve which whole
    reductions can drive that load — closed form, one curve evaluation each, no
    geometry needed — and the search then works across a spread of them and ranks
    the results together. A reduction is a means; the job is a torque at a speed.

    The band's two ends fail for different reasons and it says which. Below it the
    motor is short of torque and gearing down further helps. Above it the motor is
    short of speed, and gearing down further is what caused it — so the answer
    there is bus voltage or a different motor, and a design that fails both ends
    at once is a motor that cannot do the job at any reduction. Told apart by
    asking whether the next reduction up improves the margin, rather than by
    whether the speed ceiling has been reached: past the peak the motor is often
    still making torque, just less than the speed has taken away, and calling that
    a torque shortage sends somebody to gear down when gearing down is what did
    it.

    What the search leaves out, it says: a feasible band is usually dozens wide
    and each one costs a full search, so five are sampled and the rest are
    reported in the rejection tally. A bounded search that looks exhaustive is how
    somebody concludes a reduction does not work when it was never tried.

    On the command line it is --ratio-from-motor --out-rpm 10, and the curve
    comes off the design passed with --design rather than off eight more flags —
    the app already has a place to put those eight numbers, and a second way to
    state them would be a second thing to keep in step.

  • motor_margin joins the batch metrics, so a study can sweep bus voltage
    or reduction against what the motor can hold. nan where no curve was stated
    — the same rule the fatigue margin follows on a printed part, because a study
    that sweeps a motor across a design that has none should come back empty
    rather than come back passing.

Changed

  • GearSpec.motor is now GearSpec.motor_face. It returns the mounting
    face, and an analysis result now carries the motor's own answer under motor;
    leaving both called the same thing would have put two different objects behind
    one word on the two most-read classes in the app. MotorFrame and
    MOTOR_FRAMES moved to the new core/motor.py beside the curve and are
    re-exported from core.spec, so every existing import still resolves.

  • The panel has a Motor group. The frame and motor turns the cam used to
    be filed under Mounting with the tie bolts, because a bolt pattern is what
    they did to the geometry. That put the two halves of "which motor" in
    different boxes the moment there was a second half. Fields the chosen kind
    does not use are greyed rather than hidden — the search box already owns
    visibility in that panel, and a field that vanishes takes the reason it does
    not apply with it.

Fixed

  • A headless run wrote its files and then died on the way out, on Windows.
    import cadquery pulls casadi in whether anything uses it or not — the
    PyInstaller build already stubs it out of the frozen bundle for exactly that
    reason — and with casadi 3.8.0 the two native libraries corrupt the heap while
    they unload. The process does its work, prints its results, and exits
    0xC0000374. Nothing here calls casadi, so nothing here could avoid it;
    casadi<3.8 is pinned on Windows until the unload order is fixed upstream.

    It matters more than a red tick on a dashboard: every scripted run reported
    failure having succeeded, which is the worst way for a tool to be wrong.

    Measured rather than suspected, and it took the measurement to get there. The
    suite ended 1048 passed, 1 skipped and the step then said "exit code 1" with
    nothing else in the log, which reads as a test failure that has hidden itself.
    Four experiments said what it was not: the same failure on v7.5.0, code that
    had been green a fortnight earlier; the same failure with PySide6 pinned back
    to the version that was green; the same failure on every one of sixteen test
    modules run in its own process, and none on the other twenty-three. What the
    sixteen had in common was the CAD kernel. Then two lines were enough:
    python -c "import cadquery" crashes, python -c "import casadi" does not,
    and import cadquery on casadi 3.7.2 is clean.

  • The test step now reports the exit code it was given. The default shell on
    Windows is pwsh, which GitHub runs with $ErrorActionPreference = 'stop', and
    since PowerShell 7.4 that turns any native command failure into a terminating
    error and a flat 1. The real code was `-1073741...

Read more

cycloidgen v7.5.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 17:49

Numbers — none.

Changed

  • The application icon is the disc it cuts, not the company logo. Every
    window, task bar button, Start menu entry, installer page, Dock tile and
    desktop launcher showed the Medinstech mark — which says who wrote the
    program and nothing whatever about what it does. Somebody scanning a task bar
    full of windows is looking for this application, and a vendor logo is the
    one thing on it that is also on everything else the vendor ships.

    It is a cycloidal disc now, and not a drawing of one: tools/make_icon.py
    renders the outline through
    disc_profile(),
    the same call the STEP export cuts the part with, with the pin radius taken
    as a fraction of critical_radius() so the lobes are a disc that could
    actually be manufactured rather than a flower. The icon cannot drift from the
    geometry: change the profile and it changes with it. tests/test_icon.py
    measures the committed images back against the equation — every point of the
    outline sits at exactly the pin radius from the pin-centre locus — and against
    what the tool draws today, so a hand-edited PNG fails.

    Each size is drawn at that size. A 256 px disc with eight lobes and six
    output holes resampled to 16 px is grey soup, so it is graded like an
    optical size instead: fewer lobes, deeper, and the holes come out, until at
    16 px it is six lobes and a bore. The .ico carries seven separately drawn
    images and Windows picks between them; the macOS .icns is built the same way
    at package time and now fills every slot up to 1024, where before it stopped
    at 256 and left Finder's largest view to blur one.

    The disc is cut out of a brand-blue tile, which is what lets one asset be
    legible on a light task bar and a dark one — the bare mark was the brand
    colour against whatever the desktop happened to be, and on a dark task bar
    that is 2.8:1.

    The installer's two panels show it too, drawn at the exact 40 px and 96 px
    those controls display rather than scaled down from the 256. The Medinstech
    wordmark stays on the welcome band, where it says who publishes this; the
    picture beside it is now the icon the shortcut will get, which is what
    somebody halfway through a setup wizard is being told about.

    It is not a trademark. cycloidgen.ico and icon-*.png carry no name or
    mark of Medinstech, they are Apache-2.0 with the rest of the source, and a
    fork may keep them — see NOTICE. The brand assets beside
    them are unchanged and still are not. The two generators are kept apart on
    purpose: tools/make_assets.py needs masters that are not in this repository
    and no longer writes the icon, so a brand refresh cannot quietly put the logo
    back.


Windows - cycloidgen_..._Setup.exe. It is unsigned, so
SmartScreen will warn on first run: More info -> Run anyway. A
signed build is planned.

Linux - cycloidgen-...-x86_64.AppImage. chmod +x and run it;
it needs glibc 2.35 or newer. If it will not start, the machine has no
FUSE 2: install libfuse2, or run the file with
--appimage-extract-and-run, which unpacks it instead of mounting it.

macOS - cycloidgen-...-arm64.dmg, Apple silicon. Drag it to
Applications, then run this before opening it:

xattr -dr com.apple.quarantine /Applications/cycloidgen.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first
launch. On macOS 15 and later the dialog it raises defaults to
Move to Trash, which deletes the install - there is no Open
Anyway
in that dialog, it appears in System Settings only after a
launch has been blocked, and right-click -> Open no longer bypasses
it. Clearing the flag first avoids the dialog entirely. On an Intel
Mac, use pip.

Anywhere with Python 3.10-3.14: pip install cycloidgen, then
cycloidgen.

The numbers this produces are preliminary sizing estimates, not a
certification. Validate against a physical prototype before anything
load-bearing depends on them.

cycloidgen v7.4.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 09:18

Numbers — none.

Added

  • The app says what its output is not, where the output is made. It said it
    before, in Help ▸ About, which is a dialog nobody opens - the least-read place
    in the application for the one paragraph in it that carries a consequence. It
    is in three more places now, and they are the three that matter:

    • A strip under the export buttons, in the window, always. Not
      dismissible: a disclaimer with a close button is a disclaimer that is shown
      once. Quiet on purpose - a hairline, the warning ink, one line - because it
      has to still be legible on the hundredth session and a banner that shouts is
      read as decoration by the second.
    • A box before an export is written, every time, with no "do not show
      again". Before rather than after, because after is a notification and this
      is a decision: the files are the thing that leaves the app, gets emailed to
      a shop and outlives the session that made them.
    • NOTICE.txt, in the folder with the parts — and in the PDF dossier, on
      the first page, above the verdict. That last position is deliberate: the
      verdict says READY TO EXPORT in capitals, which is a statement about the
      checks and reads, on its own, like a statement about the design.

    The notice is written into every bundle whichever groups were selected,
    because somebody exporting drawings only is exactly the person taking a DXF
    straight to a laser cutter. It is the first entry in the export manifest and
    the first row in the README's table of outputs, and it is not a group anyone
    can untick. Asking for no groups still writes nothing at all.

    All five copies read cycloidgen.notice. The risk with a disclaimer in five
    places is not that one disappears - somebody would notice - but that one
    softens, and the weakest copy is the one that will be quoted back. A test
    holds them to the same string.

    What it says is also wider than what About used to say. It named the numbers;
    it names the geometry too. Profiles, fits and clearances come out of the same
    idealised model as the analysis, no tolerance stack has been proven, and a
    STEP file that looks finished is the easiest thing here to mistake for a
    drawing that has been checked.


Windows - cycloidgen_..._Setup.exe. It is unsigned, so
SmartScreen will warn on first run: More info -> Run anyway. A
signed build is planned.

Linux - cycloidgen-...-x86_64.AppImage. chmod +x and run it;
it needs glibc 2.35 or newer. If it will not start, the machine has no
FUSE 2: install libfuse2, or run the file with
--appimage-extract-and-run, which unpacks it instead of mounting it.

macOS - cycloidgen-...-arm64.dmg, Apple silicon. Drag it to
Applications, then run this before opening it:

xattr -dr com.apple.quarantine /Applications/cycloidgen.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first
launch. On macOS 15 and later the dialog it raises defaults to
Move to Trash, which deletes the install - there is no Open
Anyway
in that dialog, it appears in System Settings only after a
launch has been blocked, and right-click -> Open no longer bypasses
it. Clearing the flag first avoids the dialog entirely. On an Intel
Mac, use pip.

Anywhere with Python 3.10-3.14: pip install cycloidgen, then
cycloidgen.

The numbers this produces are preliminary sizing estimates, not a
certification. Validate against a physical prototype before anything
load-bearing depends on them.

cycloidgen v7.3.2

Choose a tag to compare

@github-actions github-actions released this 12 Aug 14:43

Numbers — none.

Fixed

  • The housing bore was drawn with two of every junction point, and on VTK
    9.3 that was still a hole in the housing for every ring pin. 7.3.1 replaced
    the triangulation that could not be trusted across builds, and every face it
    cuts is checked - but the mesh was handing it a loop with edges of no length
    in it: the bore arc and the pocket arc meet at the intersection, and each
    computed that point for itself, agreeing to fourteen decimal places and not to
    the fifteenth. Forty-four of them on a 21:1.

    Geometrically they cost nothing, which is why they went unnoticed for so long.
    What they cost is edges. A segment with no length has no direction, so the
    wall standing on it is a quad with no area, and whether any of that survives
    being merged back into a closed surface is up to the mesh library: VTK 9.6
    keeps the two points apart, VTK 9.3 does not, and the one that does not leaves
    the wall and the cap disagreeing about where the boundary is. Two points that
    are the same point are one point now.


Windows - cycloidgen_..._Setup.exe. It is unsigned, so
SmartScreen will warn on first run: More info -> Run anyway. A
signed build is planned.

Linux - cycloidgen-...-x86_64.AppImage. chmod +x and run it;
it needs glibc 2.35 or newer. If it will not start, the machine has no
FUSE 2: install libfuse2, or run the file with
--appimage-extract-and-run, which unpacks it instead of mounting it.

macOS - cycloidgen-...-arm64.dmg, Apple silicon. Drag it to
Applications, then run this before opening it:

xattr -dr com.apple.quarantine /Applications/cycloidgen.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first
launch. On macOS 15 and later the dialog it raises defaults to
Move to Trash, which deletes the install - there is no Open
Anyway
in that dialog, it appears in System Settings only after a
launch has been blocked, and right-click -> Open no longer bypasses
it. Clearing the flag first avoids the dialog entirely. On an Intel
Mac, use pip.

Anywhere with Python 3.10-3.14: pip install cycloidgen, then
cycloidgen.

The numbers this produces are preliminary sizing estimates, not a
certification. Validate against a physical prototype before anything
load-bearing depends on them.

cycloidgen v7.3.1

Choose a tag to compare

@github-actions github-actions released this 12 Aug 13:05

Numbers — none. Nothing computed moves in this release: it is the 3D view's
geometry and the interpreters the package will install on.

Fixed

  • Parts in the 3D view were not watertight on macOS or on VTK 9.3, so a
    section cut through them read as a hollow shell rather than as solid
    material — the exact fault 7.2.0 said it had fixed, on the two platforms it
    had not been run on.

    The cause is worth stating plainly, because it is a lesson rather than a
    slip. Faces with holes in them were filled by vtkContourTriangulator, which
    gives up part way on some inputs and reports nothing about it. 7.2.0 added a
    check for that and a list of angles to turn the face by and try again — a
    list searched, on one machine, for the smallest set that cleared every face
    this app can draw. But which inputs defeat that filter is a property of the
    build, not of the geometry: the same list left the housing full of holes on
    VTK 9.3 (which is what Python 3.10 gets) and the end cap full of holes on
    macOS arm64 at the very version it was developed on. Neither reproduces here.

    So the face is cut here now, in viz/tessellate.py, by the textbook method
    for a polygon with holes: a sweep adds the diagonals that leave nothing but
    monotone pieces, the pieces are traced out of the loops and those diagonals
    together, and each is triangulated by the stack walk. Every step is
    comparisons and cross products in a fixed order, so two machines get the same
    triangles — and the suite checks all of it directly: the whole area, no
    directed edge twice, and a boundary exactly as long as the loops.

    It is also faster, and by more than the algorithm: the triangles come back
    as indices into the mesh's own vertices, so a cap shares its corners with the
    walls that meet it and there is nothing to append and merge afterwards, and
    the cutting is remembered per mesh rather than repeated for each of the two
    surfaces every part needs. A full rebuild of the 21:1 view went from 0.18 s to
    0.11 s, and rebuilding a mesh already seen from 0.18 s to 0.02 s.

    Two things came out of writing it. Nothing in the suite had asked that a
    face's triangles all be wound the same way
    — and the area cannot see it, so
    a face covered twice over and once backwards passes on the sum. And the
    housing's bore is drawn as a bore arc and a pocket arc per pin, meeting at a
    point each of them computes for itself: the same point to fourteen decimal
    places, which is an edge of no length to a sweep and cannot simply be dropped,
    because the wall below the cap keeps that edge and a boundary only one of them
    keeps is a hole.

Changed

  • Python 3.13 and 3.14. requires-python said <3.13 on a tree whose whole
    suite passes on 3.14, so pip install refused an interpreter the code was
    fine on. The bound is <3.15 now. An upper bound is still stated rather than
    dropped — this depends on OCCT, VTK and Qt through binary wheels, and an
    interpreter no wheel exists for is a failed install however permissive the
    metadata is — and it moved to where there is a run behind it: 3.13 and 3.14 on
    Linux, and 3.14 on Windows as well, because those wheels are built per
    interpreter and per platform. 3.12 stays the version every bundle is frozen
    on.

Windows - cycloidgen_..._Setup.exe. It is unsigned, so
SmartScreen will warn on first run: More info -> Run anyway. A
signed build is planned.

Linux - cycloidgen-...-x86_64.AppImage. chmod +x and run it;
it needs glibc 2.35 or newer. If it will not start, the machine has no
FUSE 2: install libfuse2, or run the file with
--appimage-extract-and-run, which unpacks it instead of mounting it.

macOS - cycloidgen-...-arm64.dmg, Apple silicon. Drag it to
Applications, then run this before opening it:

xattr -dr com.apple.quarantine /Applications/cycloidgen.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first
launch. On macOS 15 and later the dialog it raises defaults to
Move to Trash, which deletes the install - there is no Open
Anyway
in that dialog, it appears in System Settings only after a
launch has been blocked, and right-click -> Open no longer bypasses
it. Clearing the flag first avoids the dialog entirely. On an Intel
Mac, use pip.

Anywhere with Python 3.10-3.14: pip install cycloidgen, then
cycloidgen.

The numbers this produces are preliminary sizing estimates, not a
certification. Validate against a physical prototype before anything
load-bearing depends on them.

cycloidgen v7.3.0

Choose a tag to compare

@github-actions github-actions released this 12 Aug 10:19

Numbers

  • The disc turns against the crank, and the app had them subtracting. Every
    speed measured between the disc and the crank was computed as
    input_rpm * (1 - 1/i). It is 1 + 1/i: the disc rotates the opposite way
    from the crank — that is what a fixed-ring cycloidal drive is — so the two
    rates add rather than cancel.

    Three places carried it. The eccentric cam bearing, which separates those
    two bodies and so turns at their difference; the output pin rubbing speed,
    which is the disc walking round the carrier at that same rate; and the
    output stage's sweep period, derived from how fast the eccentricity
    direction runs round as seen from the carrier.

    What gave it away was the pin-in-hole constraint. Relative to the carrier the
    disc translates on a circle of radius E, so at every crank angle each output
    pin has to sit exactly E from its hole centre — and it does, only with the
    carrier turning at +phi/N. Put that rate in and the eccentricity direction
    seen from the carrier advances at (N+1)/N, not (N-1)/N. The two stage
    periods then agree for the first time: the output period comes out as the ring
    period divided by the output pin count, which is what a pattern of n repeats
    in one turn of the ring pattern has to be. They were derived independently and
    only the correct rate makes them meet.

    Measured over the presets:

    Quantity 10:1 21:1 59:1
    Cam bearing speed, output PV, output sliding speed +22.2% +10.0% +3.5%
    Cam bearing L10 life −11% −9.1% −3.3%
    Input shaft support speed +10.0% +4.8% +1.7%
    Running temperature +4.2% +1.7% +0.3%
    Input torque for the rated output +1.7% +1.0% +0.4%
    Efficiency −1.2 pt −0.7 pt −0.3 pt
    Torsional stiffness +2.6% +4.5% +5.6%

    It is worst where it matters most — a low ratio is where that bearing is
    fastest — and everything on the ring side is unchanged, which is the expected
    result rather than a reassuring one: nothing about the ring contact was ever
    measured against the crank.

  • The two input shaft supports do not turn at the same speed, and both were
    quoted at the input speed. One sits in an end plate and one in the carrier's
    boss, and those two bodies move differently. The schedule still carries one
    row — the seats are the same size, so one part number does for both — but it
    is sized on the faster of the two and prints both.

  • The output pin rollers were sized against the input speed, which is
    neither the speed of anything at that contact nor the frequency of anything.
    They are now counted on the rate the hole walks round the pin, which is the
    cycle they are actually loaded on.

Which member is the output

  • Either of the two slow members can now be the output. A cycloidal drive is
    a three-shaft machine: the crank is the input, and the ring and the carrier are
    interchangeable. Ground the ring and the carrier turns at N:1, reversed —
    which is what the app has always built. Ground the carrier and the housing
    turns, at N+1:1, in the same direction as the input. Same parts, one more
    tooth of reduction, and it is what most printed micro drives are.

    output_member selects it, and everything downstream follows rather than
    being told twice:

    • The reduction and the direction come off the two members' rotation rates, so
      ratio cannot disagree with the picture drawn from the same numbers.
    • Every rate in the app is now stated per unit input speed rather than per
      unit crank angle, because those part company here: with the carrier
      grounded the crank runs at (N+1)/N of the input. Relative speeds inside
      the drive are unchanged by the choice, which they must be — grounding a
      member adds one rigid rotation to all of it at once.
    • The motor moves to whichever member stands still. On a ring-output drive
      the carrier grows a base at the end of its boss, carrying the motor's
      pattern and register; the input end plate loses them and gains an output
      bolt circle for the driven machine instead, on the tie bolts' own circle
      and half a pitch round from them.
    • The 3D view, the 2D mechanism view and the exported animation turn the part
      that actually turns. One rigid frame rotation applied to the whole assembly,
      not a second set of motion laws to keep in step with the first.
    • The design search knows the difference between a reduction and a lobe count:
      a 30:1 off the ring is a twenty-nine lobe disc.
  • A ring-output drive is a frame, not a plate. The grounded member cannot be
    a carrier hanging on six cantilevered pins with a barrel swinging off one
    bearing, and the reference builds are not: the pins land in an end cap at
    their far end and become the frame's own fasteners, so the carrier, the pins
    and the cap are one rigid cage that the housing turns inside. That is a new
    made part, end_cap, with its own STEP, STL and line on the bill of
    materials, and it changes two numbers that matter.

    The output pins are beams. A pin built into one plate carries F*L; the
    same pin caught at both ends carries F*a*b/L. On the 21:1 preset with two
    discs that is 45.3 MPa of fully reversed bending against 19.9 MPa. Their
    stiffness moves the other way and the app says so - the span is now the
    whole pin rather than half a stack, so the structure comes out slightly softer
    (7.26 to 6.57 Nm/arcmin) even though the case is stiffer.

    The housing is carried at both ends. One bearing locates a barrel and does
    not hold one against a moment, which is exactly what a wheel or a pulley on a
    turning barrel applies. There is a main output bearing on each of the frame's
    two bosses now, and both shaft supports move inside those bosses - the one
    that used to sit in the input end plate had to, because that plate turns.

    It costs length: the barrel has to cover the cap, so the 21:1 preset goes from
    40 mm to 63 mm and from 781 g to 1033 g.

  • The output pin's bending arm starts at the carrier's face, which is a
    carrier drop below the first disc, and it was being measured from the disc.
    Small - one millimetre on a moment arm - but it is a moment arm, and it pushed
    a 16 mm disc stack on the 15:1 preset from just inside the output pin's
    fatigue limit to just outside it. FATIGUE_LIFE says so now. Carrier-output
    drives are affected, which is all of the ones that existed before this
    release.

  • The carrier's base was drawn one millimetre off the boss it stands on.
    Introduced with the base itself, earlier in this release, and never shipped:
    the part is modelled in its own frame and an assembled height was used in it
    without the carrier drop, so the exported solid was in two pieces. The volume
    was right, which is why nothing caught it - a printer would have made both.

  • OUTPUT_BOLT_CLASH, a new check. The output face's bolts share a circle
    with the tie bolts, because that is the one radius on that plate with barrel
    wall behind it to thread into. Equal counts interleave exactly; seven against
    six leaves 0.12 mm of metal; twelve against six lands one hole on another.
    Fifty-six checks.

Ring pins the housing is printed with

  • The ring pins can be formed with the housing instead of fitted into it as
    separate dowels — ring_pins_integral, and the case every printed drive is.
    A pocket and the pin that fills it are one shape read from either side, so
    almost the whole of the difference is which arc of the same circle the bore
    follows, the outward half or the inward one, and cut against union in the
    exporter.

    The pins stop being a part when they are integral: no body in the 3D view, no
    STL, no line on the bill of materials — twelve lines to eleven, six bought
    parts to five — and no visibility row for something you can neither see
    separately nor take out. Their mass moves into the barrel and into the
    housing's material, which is the point of the option: on the 21:1 preset the
    drive goes from 781 g to 623 g, because 188 g of steel dowels become 30 g of
    the printed material that was going to be hollowed out to seat them.

  • They also stop being able to roll, and that is not free. An integral pin
    cannot turn in a pocket it is part of, so the drive that pays for this is the
    one that had rolling ring pins. Same 21:1, rollers on:

    Quantity Rolling dowels Formed with the housing
    Efficiency 83.7% 70.4%
    Ring pin loss 1.00 W 6.63 W
    Running temperature 29.5 °C 40.5 °C
    Ring pin sliding duty rolls 2.2x the PV limit

    PV_LIMIT_RING fires there rather than the trade being made quietly: PLA on
    steel at 0.22 MPa and 0.30 m/s is a disc that wears round long before it
    breaks. The ring pin roller leaves the bearing schedule with it, five rows to
    four, which is the other half of the same fact — there is no longer a part
    free to turn for a needle to sit under.

  • ring_pins_roll is what every consumer asks now, and it is derived rather
    than enforced. The obvious way is a validator that clears
    ring_pins_are_rollers when the pins are integral, and it does not work:
    model_copy runs none, so a spec that arrived that way kept rolling pins on
    an integral ring and its efficiency came back unchanged — which is how this
    was caught. Deriving it also keeps the roller preference for when the pins
    stop being integral, which is why the box is greyed rather than cleared. The
    field is in mesh_fingerprint for the same class of reason: it changes the
    mesh, and a key that did not carry it would have served the old one.

Added

  • 3MF export — ...
Read more

cycloidgen v7.2.0

Choose a tag to compare

@github-actions github-actions released this 11 Aug 11:47

Numbers

  • Every sweep in the app was running over a window that is not a period, and
    the numbers taken over it have moved.
    A lobe pitch, 360/N, is the period
    of the disc's shape. It is not the period of anything a sweep samples: a
    sweep samples the pins, and there are N+1 of those against N lobes, which
    is the entire mechanism. Pin k meets the profile at t_k = phi/N - 2*pi*k/(N+1), so the crank has to turn 360*N/(N+1) before each contact
    lands where its neighbour was — 330 degrees on the default drive, not 32.7.

    What gave it away is that the curve did not close. Peak pin force reads
    49.859 N at phi = 0 and 49.271 N one lobe pitch later, so the Ring pin
    load
    plot was a tenth of a cycle cut at an arbitrary phase, ending somewhere
    other than it began. Tiled, it steps at the seam. That is what it looked
    like, and it is what it was.

    The ring stage now sweeps 360*N/(N+1) and the output stage sweeps its own
    360*N/(n*(N-1)), which is a different number and always was — the two have a
    common multiple that runs to thirty input revolutions on some tooth counts,
    which is exactly why neither can be swept on the other's window. Each stage
    gets its own loop. This is not an approximation: a maximum per stage is a
    maximum per stage, and the mean of a sum is the sum of the means.

    Measured against 7.1.1, over the presets and several tooth counts:

    Quantity Worst change
    Ring peaks — pin force, contact pressure, torque capacity under 0.01%
    Efficiency, running temperature 0.6%
    Mean sliding speed 0.7%
    Output pin force, output PV, disc web shear 5.5%
    Torsional stiffness 12.6%
    Load concentration 8.6%
    Transmission error 7.4%

    The ring-side peaks barely move, and that is the expected result rather than a
    reassuring one: a maximum over the pins does not care which phase of the cycle
    you start at, only whether you covered it. What moves is everything averaged
    or ripple-measured, and the output stage, which was being swept over about
    half its period.

  • A check that should have been firing was silent. On the 29:1 preset the
    output pin contact pressure exceeds the allowable, and HERTZ_STRESS_OUTPUT
    now says so. It did not before, because the peak output pin force was sampled
    over a lobe pitch — half the output stage's period — and came out 5.3% low,
    which was the wrong side of the limit. Anyone who took that preset as passing
    should re-run it.

  • ring_period_deg in the transmission-error result was the lobe pitch, on
    a result type whose other field, output_period_deg, has been correct since
    it was written. It now reports the ring period: 330 degrees rather than 32.7
    on the default drive.

  • Sweeps are 144 steps rather than 72. Sampling an exact period uniformly is
    unbiased at almost any count, so this is not what fixed the window — it is
    only about resolving the peak. Against a 20000-step reference, 72 steps miss
    peak pin force by 0.11% at 30 lobes and 144 by 0.004%.

  • The transmission error's ring sweep needed four times the steps, and this
    one is about resolution. Twelve samples were enough across a lobe pitch and
    are less than one per pitch across the ring period, which read 11% low on the
    ring share; forty-eight is where it stops moving. What is left is bounded by
    the shared sweep rather than by that number, and is stated in the source: the
    ring share sits about 2% under a sweep four times finer, 0.7% on the total.
    Closing it means quadrupling the sweep every other study reads, and a
    transmission error 2% conservative on one of its two halves is not what limits
    this model.

Fixed

  • No part in the 3D view was watertight, so the section could not cap them.
    A cut came out with some parts reading as solid material and others as empty
    shells — half the assembly sectioned, half of it hollow.

    The faces are emitted one at a time and each brings its own copy of every
    corner, so no face shared an edge with its neighbour: geometrically solid,
    topologically a heap of loose facets, every edge in it a boundary edge.
    vtkClipClosedSurface caps a closed surface and could not cap any of the
    fourteen.

    The comment on the filter that was supposed to prevent this said it merged
    the duplicate points. vtkPolyDataNormals does not merge points — with
    SplittingOn it creates them, which is what gives a cylinder's end cap a
    hard edge against its wall and is the right thing for shading. So the points
    are merged first and split afterwards, and the two surfaces have different
    jobs: the section and the edges take the closed one, the shading takes the
    split one. Twelve of the fourteen parts are now watertight.

    The same duplication had been doubling the edge overlay, which found every
    edge twice — once from each of the two faces that should have been sharing
    it. 12,614 line segments where 6,253 do.

    Two parts are still not clean, for reasons of their own, and are not fixed
    here: output_flange has 28 non-manifold edges where the plate and the boss
    both keep the face they meet on, and disc_1 has a four-edge hole in its top
    face where vtkContourTriangulator gives up on one arrangement of output
    holes and the code checks only that it produced some triangles. disc_2,
    the same part on a different hole phase, is clean.

  • The screenshot tool drove the operator's real preferences. It forced the
    light theme, cleared the section plane and unhid the 3D groups, then put them
    back at the end — and anything that raised in between skipped the putting
    back, which is how a window starts opening in the wrong theme with no sign of
    why. It runs against a throwaway settings file now, which is what
    settings.ENV_VAR exists for and is a better answer besides: these images are
    meant to show a fresh install, and now they are taken on one.

  • The output stage was swept over a lobe pitch in four more places
    analyse_contacts, analyse_efficiency, the thermal solve, and the disc-web
    and output-pin fatigue checks. output_stage_period had been in the codebase
    since the transmission-error work, with a docstring warning that sweeping a
    lobe pitch "reports about half the ripple that is really there", and nothing
    outside that one function called it. Both period functions live in
    core.kinematics now, next to the sweeps that need them.

  • Nothing in the suite asserted that a period was a period, which is why
    this survived four releases and 788 tests. There are now tests that advance
    each stage by its own period and require the load pattern to return — one pin
    along for the ring stage, one the other way for the output stage, because the
    pattern steps onto its neighbour rather than staying put. And a test that
    holds the mistake down directly: a lobe pitch must not close either stage,
    compared as multisets so that renumbering the pins cannot rescue it.

Changed

  • The drawing says what speed it is showing, and the playback control says
    what "1x" means.
    These were one confusion. The animation runs at 3 degrees
    of input per 33 ms frame, which is one input revolution every four seconds —
    15.2 rpm — while the tooltip claimed "input revolutions per second of wall
    clock", four times faster than the thing it described. The control is
    labelled PLAYBACK now and carries a live readout of the rate it actually
    turns at, so the multiplier is answerable without a tooltip; the rate is
    derived from the two timing constants rather than written down beside them.
    The drawing carries the design's speed, which is the other half of the
    question: a picture turning visibly at fifteen rpm, describing a drive rated
    at a thousand, needs to say which of the two it is.

  • The drawing and the datasheet name the arrangement. Ring fixed, output
    taken from the disc's pin holes through the carrier — the planetary
    configuration, as against grounding the carrier and driving the ring, which
    is the star configuration and gives Np rather than N. The reduction line
    and the output-speed line both now say the output turns against the input,
    which the geometry has always done and nothing ever mentioned:
    output_rpm has no sign to carry it. The README says which member is
    grounded, why that fixes the ratio and the direction, and that the disc rolls
    on the inside of the pin circle.

  • The explanation panel answers two questions and sat beside one of them.
    It explains the selected check and the parameter you clicked, and it lived
    in the bottom-right corner — so clicking a parameter in the left-hand panel
    put the reply as far from the question as the layout allowed. It is under the
    parameters now, and the checks list has the full width it had been competing
    for. That also retires the machinery that used to hide the panel on a narrow
    window and hold a floor under the detail column: nothing competes for that
    width any more.

  • The 3D tab's controls are grouped by what they do. Explode was alone at
    the top beside the view buttons while section was squeezed onto the end of the
    visibility checkboxes at 150 px, on a row that had already run out of width
    and wrapped. They are the same kind of control — drag to open the assembly up
    — and they share the top row now, at the same size. The second row is
    visibility and nothing else. No extra height.

  • The wrapping toolbar lined its widgets up by their boxes, not their
    contents.
    Everything was placed at the top of its row, so the bearings menu
    — a few pixels taller than the checkboxes beside it — painted its own glyph
    low and read as dropped punctuation. Rows are measured before they ...

Read more

cycloidgen v7.1.1

Choose a tag to compare

@github-actions github-actions released this 07 Aug 15:34

Numbers

  • Nothing computed moved. Every quantity this reports is what 7.1.0
    reported, to the digit — the export from the trimmed bundle was compared file
    by file against one built with every dependency present, and all 29 came out
    the same size. This release is packaging, documentation and one dialog.

Fixed

  • "Design for requirements" could not be made to fit a short screen, which
    put the buttons that end it out of reach. Four group boxes and the Search
    button came to 832 px of minimum height; a dialog inherits the minimum of what
    is inside it, so the window could not be sized below 885 px with its frame on
    resize(1080, 660) asked for 660 and silently got 854. On anything shorter
    than that, the Use this design and Cancel box at the bottom of the results
    column sat off the bottom of the screen, and dragging the edge did nothing,
    because the window was already as small as it was allowed to be. A 1366x768
    laptop is short enough. So is a 1080p panel at 150% scaling, where the
    logical height is 720.

    The requirements form now scrolls, which is what breaks the inheritance: the
    dialog's minimum height goes from 854 px to 156 px and it is bounded by the
    results column, which stretches, rather than by the tallest thing in it.

    Search stays outside the scrolled part, pinned under it. Putting it in with
    the fields would have fixed the reaching problem by handing it to the button
    the dialog exists to have pressed — on a short screen it would have been below
    the fold, which is where the button box was to begin with.

    The failure was not that it looked cramped. Both buttons that close the dialog
    were unreachable, so the feature could be opened and not used.

  • The macOS instructions deleted the application. They offered System
    Settings ▸ Privacy & Security ▸ Open Anyway
    first and xattr -dr com.apple.quarantine as an alternative, which reads as "open it, then deal
    with the warning". On macOS 15 and later that warning's default button is
    Move to Trash. A tester on macOS 26 followed the README exactly and watched
    the 1.3 GB install go to the Trash without the application ever running —
    Gatekeeper blocked the launch, then syspolicyd moved the bundle. Open
    Anyway
    is not in that dialog at all; it appears in System Settings only
    after a launch has already been blocked, and right-click ▸ Open stopped
    bypassing Gatekeeper in macOS 15.

    So the order is reversed everywhere it appears — the README, the generated
    release notes and RELEASING.md — and the clearing step now comes before the
    first launch, where it produces no dialog at all. A test holds the order,
    because it is prose in two files, one of them a heredoc inside a workflow,
    and reflowing it back the wrong way is a plausible tidy-up.

    Being unsigned was known and stated. What was not stated is that the easiest
    click destroys the install, which is the difference between a warning and a
    trap.

  • --version from the .app does not print while the bundle is
    quarantined.
    The README explained why a windowed macOS build still answers
    on the command line and offered the command as proof. True for the bundle the
    release workflow builds, which never carries the flag, and false for the one
    a user downloads: Gatekeeper blocks the exec as well as the double-click,
    so the command hangs for about ten seconds and prints nothing. It answers
    normally once the quarantine flag is cleared, and the claim now says so.

    Both of these came from a tester on real hardware. Neither could have been
    caught here: the release workflow runs the binary out of the mounted image
    and it passes, because a file the workflow built itself was never quarantined
    in the first place.

Changed

  • The bundle is 790 MB, down from 1.2 GB. 444 MB of it, 36%, was three
    packages that CadQuery declares as dependencies and this application never
    imports: casadi, an interior-point optimiser, at about 220 MB; numba with
    LLVM behind it at 142 MB; and trame, a browser viewer, at 20 MB. CadQuery
    declares what CadQuery can do, not what one caller uses.

    The note this replaces said the bundle was "essentially all OCCT" and
    suggested a build without the CAD kernel for anybody who cared. Both halves
    were wrong. OCCT is 152 MB — 12% — and the optimiser that arrives beside it is
    larger than it is, so the prescribed remedy would have given up STEP and STL
    export, which is half the point of the tool, to save a third of what came off
    by shipping the same features with less beside them. The size was a plausible
    guess nobody had weighed; measuring it took one pass and reversed the answer.

    Nothing is dropped on the strength of not finding an import. Each package is
    made unimportable and the whole suite is run against that — 778 tests — and the
    export is compared file by file against one from an environment with every
    dependency present: 29 files, identical sizes, kernel path included.

    casadi needed more than an exclusion, because it is imported whether it is
    used or not: cadquery/__init__.py reaches occ_impl.solver, which imports it
    at the top. So packaging/rthook_casadi.py stands in for it, and what makes
    the substitution safe is where the module uses it — all thirty-seven
    references are inside function bodies, and the only names that run at import
    are two annotations. Reading a name off the stand-in works, which is all an
    annotation needs; calling one raises with a sentence saying the solver was left
    out. This application constrains no assemblies — every part is placed by an
    explicit transform off the kinematics — so the limit is real but unreachable
    from the application, and pip install cycloidgen brings the genuine article.

    Not done, and measured rather than left vague: about 123 MB of VTK is never
    loaded either. It is left in because it cannot be checked the way the rest
    was. Excluding a VTK module is a missing DLL in a frozen build, not a failed
    import in a virtual environment, so the test suite would go green on a bundle
    that had lost the 3D view.


Windows - cycloidgen_..._Setup.exe. It is unsigned, so
SmartScreen will warn on first run: More info -> Run anyway. A
signed build is planned.

Linux - cycloidgen-...-x86_64.AppImage. chmod +x and run it;
it needs glibc 2.35 or newer. If it will not start, the machine has no
FUSE 2: install libfuse2, or run the file with
--appimage-extract-and-run, which unpacks it instead of mounting it.

macOS - cycloidgen-...-arm64.dmg, Apple silicon. Drag it to
Applications, then run this before opening it:

xattr -dr com.apple.quarantine /Applications/cycloidgen.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first
launch. On macOS 15 and later the dialog it raises defaults to
Move to Trash, which deletes the install - there is no Open
Anyway
in that dialog, it appears in System Settings only after a
launch has been blocked, and right-click -> Open no longer bypasses
it. Clearing the flag first avoids the dialog entirely. On an Intel
Mac, use pip.

Anywhere with Python 3.10-3.12: pip install cycloidgen, then
cycloidgen.

The numbers this produces are preliminary sizing estimates, not a
certification. Validate against a physical prototype before anything
load-bearing depends on them.