Advertisement
creamygoat

Stargate Model Testbed

Jun 22nd, 2023
638
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 75.21 KB | Source Code | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. """Stargate Model Testbed
  4.  
  5. This program animates a minimalistic abstract model of a Pegasus
  6. stargate (with Milky Way gate emulation) to be incorporated into
  7. a microcontroller.
  8.  
  9. ---
  10. Stargate Model Testbed
  11. Version: 1.0.0.0
  12. (c) Copyright 2023, Daniel Neville (creamygoat@gmail.com)
  13.  
  14. This program is free software: you can redistribute it and/or modify
  15. it under the terms of the GNU General Public License as published by
  16. the Free Software Foundation, either version 3 of the License, or
  17. (at your option) any later version.
  18.  
  19. This program is distributed in the hope that it will be useful,
  20. but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. GNU General Public License for more details.
  23.  
  24. You should have received a copy of the GNU General Public License
  25. along with this program. If not, see <https://www.gnu.org/licenses/>.
  26. ---
  27.  
  28. The animation of the stargate by this program is not necessarily
  29. a faithful representation of the appearance of the stargate. Rather,
  30. the animation is to prove that the compact model contains enough
  31. information at any given moment for a sound generator and a renderer
  32. or LED driver to fully represent the state of an animating stargate.
  33.  
  34. Proposed Input Control Lines (or commands via serial):
  35.  Incoming
  36.  Open
  37.  Close
  38.  Style (2)
  39.  Sequence (4)
  40.  Colour (2)
  41.  Delay (analogue, represents sound propagation delay correction)
  42.  Serial (for programming constellation and chevron sequences)
  43.  
  44. Output Control Lines (for external audio and visual devices):
  45.  Speed (Fast PWM)
  46.  Rotating
  47.  Lurch (if acceleration used)
  48.  Click
  49.  Clack (if Milky Way emulation used)
  50.  Opened
  51.  Clunk (all chevrons reset)
  52.  
  53. Control lines will need to be robustly protected from ESD,
  54. grounding issues and wire self-inductance. (Just an 8m pice
  55. of wire can destroy a microcontroller.) An ideal second
  56. control interface would be a second microcontroller with an
  57. Ethernet adaptor, since network interfaces are galvanically
  58. isolated and well protected.
  59.  
  60. """
  61.  
  62. import os
  63. import pygame as pg
  64. import copy
  65. import numpy as np
  66. import numpy.linalg as la
  67. from enum import IntEnum
  68. from enum import auto
  69.  
  70. help_msg = """
  71. Stargate Model Testbed
  72.  
  73. [ESC], [Q] Quit
  74. [P] Power
  75. [X] Cut power
  76. [O] Dial out
  77. [I] Incoming
  78. [C] Close
  79. [S] Select stargate style
  80. [H] HUD on/off
  81. """
  82.  
  83. def sqrt_int32(x):
  84.   """Return floor of the square root of a 32-bit integer."""
  85.   x1 = x
  86.   c = 0
  87.   d = 1 << (32 - 2) # Second-to-top bit set
  88.   while d > x:
  89.     d >>= 2
  90.   while d:
  91.     if x1 >= c + d:
  92.       x1 -= c + d
  93.       c = (c >> 1) + d
  94.     else:
  95.       c >>= 1
  96.     d >>= 2
  97.   return c
  98.  
  99.  
  100. def chevron_pos(n):
  101.   """Return a zero-based chevron position index given a chevron number.
  102.  
  103.  The chevron numbers might not be standard. Here, Chevron 7 is at
  104.  the top, where the click-clacking indexer is on the Milky Way gates.
  105.  Chevrons 8 and 9 are at the lower right and left respectively. The
  106.  remaining chevrons are arranged clockwise right from the 1 o'clock
  107.  position (on a nine-hour clock face).
  108.  
  109.         7
  110.    6         1
  111.  
  112.  5   Chevron   2
  113.      Numbers
  114.   4           3
  115.  - - - - - - - -
  116.      9     8
  117.  
  118.  Chevron position indices run clockwise starting at zero from the top.
  119.  
  120.         0
  121.    8         1
  122.      Chevron
  123.  7   Position  2
  124.      Indices
  125.   6           3
  126.  - - - - - - - -
  127.      5     4
  128.  """
  129.   x = n
  130.   if x < 7:
  131.     if x == 0:
  132.       x = -1
  133.     elif x >= 4:
  134.       x += 2
  135.   else:
  136.     if x == 7:
  137.       x = 0
  138.     elif x < 10:
  139.       x -= 4
  140.     else:
  141.       x = -1
  142.   return x
  143.  
  144.  
  145. # The Milky Way emulation features a rotating ring patterned
  146. # with slight colour variations to show rotation.
  147.  
  148. def build_mw_ring_colours():
  149.   contour = "999865365122474210011324467522335678"
  150.   contour = contour[:36]
  151.   contour += "0" * (36 - len(contour))
  152.   x = 0.3 + 0.6 * np.array([(ord(c) - ord("0")) / 9.0 for c in contour])
  153.   x = np.array(np.rint(255 * x))
  154.   result = np.hstack([x, x, x])
  155.   return result
  156.  
  157.  
  158. def ring_colour_at(angle, mw_ring_colours):
  159.   """Return an interpolated colour at a 24-bit angle on the ring.
  160.  
  161.  This interpolation is used in Milky Way stargate emulation mode in
  162.  order to vaguely show a rotating ring on a gate built as a Pegasus
  163.  stargate.
  164.  
  165.  The angle ranges from 0x000000 to 0x900000. Interpreted as a 6.18
  166.  fixed-point binary format, the (absolute) angle represents the
  167.  number of constellation sectors clockwise from top dead centre.
  168.  """
  169.   a = angle
  170.   while a < 0:
  171.     a += 0x900000
  172.   while a >= 0x900000:
  173.     a -= 0x900000
  174.   s0 = a >> 18
  175.   s1 = s0 + 1
  176.   if s1 >= 36: s1 = 0
  177.   f = (a & 0x03FFFF) / 0x040000
  178.   col0 = np.array(mw_ring_colours[s0])
  179.   col1 = np.array(mw_ring_colours[s1])
  180.   col = np.rint(col0 + f * (col1 - col0))
  181.   col = np.maximum([0, 0, 0], np.minimum([255, 255, 255], col))
  182.   col = np.array(col, dtype=np.uint8)
  183.   return col
  184.  
  185.  
  186. class SgStyle (IntEnum):
  187.   """Stargate style integer enum
  188.  
  189.  Two styles, Pegasus and Milky Way are base styles with accurate
  190.  timings. Other styles are modifications of the base styles.
  191.  
  192.  A Pegasus stargate, such as the one on Atlantis is blue (sometimes
  193.  sea-foam green) chevrons and a fixed ring of thirty-six constellation
  194.  displays. During dialling, each constellation to be dialled roams from
  195.  chevron to chevron, settling at the target chevron before spawning the
  196.  next constellation. (The first constellation starts at the 1 o'clock
  197.  position and propagates anticlockwise.) There is no indexer chevron
  198.  but the top chevron, as with the Milky Way gate is always the last to
  199.  be "locked" (illuminated) and is associated with the home constellation
  200.  (which is unique to each stargate and has no addressing function, but
  201.  is presumably used to ready the stargate's ring by bring it to the
  202.  home position (at angle zero).
  203.  
  204.  The Milky Way stargate has red-orange chevrons and an alternating
  205.  rotating ring of thirty-nine constellations and a moving click-clacking
  206.  indexing chevron at the top which also functions as the last chevron to
  207.  accept a constellation, which must always be the "home" constellation
  208.  (which is unique to each stargate and only meaningfully represents the
  209.  home angle position on the ring). During dialling, each selected
  210.  constellation is brought to the indexing chevron, which briefly opens
  211.  with a click, illuminates in synch with another chevron being locked,
  212.  closes with a clack and (if not the final chevron), extinguishes. The
  213.  lower chevron frame is distinct by both its movement and its four bright
  214.  hot spots illuminating its chevron frame. The Milky Way emulation mode
  215.  used in this model designed for a Pegasus theatre prop, uses only
  216.  thirty-six constellations.
  217.  """
  218.   Pegasus = 0
  219.   MilkyWay = 1
  220.   Pegasus_Accel = 2
  221.   MilkyWay_Fast = 3
  222.   Pegasus_Fast = 4
  223.  
  224.  
  225. class SgState (IntEnum):
  226.   """Major state number of the stargate finite state machine"""
  227.   Off = 0
  228.   Idle = 1
  229.   PreDial = 2
  230.   Dialling = 3
  231.   Misdialled = 4
  232.   FinalChevron = 5
  233.   AlignForIncoming = 6
  234.   Incoming = 7
  235.   Opening = 8
  236.   Open = 9
  237.   Closing = 10
  238.   Dimming = 11
  239.   Resetting = 12
  240.  
  241.  
  242. class StargateParam():
  243.  
  244.   """Stargate configuration parameters
  245.  
  246.  The parameters are constant for any given style and are intended
  247.  to be shared by StargateState instances of any given style.
  248.  
  249.  Fields
  250.  
  251.  dial_sequence = array[0..8] of 0..35
  252.    For an emulated Milky Way gate the ring is rotated to the
  253.    constellation sectors numbered clockwise from 0 at the home
  254.    symbol. (This means that when constellation 3 is selected,
  255.    say, the ring is rotated 3 sectors anticlockwise from TDC
  256.    to an absolute angle of 33 sectors.) A proper Milky Way
  257.    gate, not emulated here, has 39 constellation sectors.
  258.    A Pegasus gate is supposed to have the ability to display
  259.    any given constellation at any desired sector during the
  260.    dialling sequence. For a prop which has constellation LEDs
  261.    in fixed positions, the dial_sequence field has no meaning,
  262.    since the roving constellation cursor is only able to light
  263.    some or all of the LEDs actually present.
  264.  
  265.  lock_sequence: array[0..8] of 0..9
  266.    The lock sequence is an array of chevron numbers (1..9)
  267.    terminated by a zero or the array length. The sequence
  268.    determines the order in which the chevrons are locked.
  269.    Usually the sequence is
  270.      [1, 2, 3, 4, 5, 6, 7],
  271.      [1, 2, 3, 4, 5, 6, 8, 7] or
  272.      [1, 2, 3, 4, 5, 6, 8, 9, 7].
  273.    Non-standard ordering are permitted. This may be useful
  274.    for a theatre prop lacking distinguishability between
  275.    addresses. Short sequences are permitted. If me_emulation
  276.    is True, the last chevron should be chevron number 7
  277.    (or whichever chevron is at chevron position zero, at TDC)
  278.    in order for the indexing chevron to animate properly.
  279.  
  280.  num_good_chevrons: 0..9
  281.    A misdial can be induced by setting num_good_chevrons less
  282.    than the length of the lock sequence before (which may be
  283.    shortened with a zero entry). The "bad" chevron is indicated
  284.    by a failure to light the chevron or activate the indexer,
  285.    a pause, then the release of all previously locked chevrons.
  286.  
  287.  style:
  288.    An enum for the kind and variant of stargate. The style is set
  289.    by a call to set_canonical_style or set_style as the function
  290.    sets the parameters accordingly, but is not used by the finite
  291.    state machine. It is a convenience field for use by the main
  292.    function and the renderer.
  293.  
  294.  base_style:
  295.    An enum for the kind of stargate. Base styles allow the user
  296.    to create many style variations that are modifications of base
  297.    styles so only the modified parameters need be coded by the user.
  298.    Just as with the style field, base_style is not used by the
  299.    finite state machine. It is a convenience field for use by the
  300.    main function and the renderer.
  301.  
  302.  mw_emulation: Boolean
  303.    Enables Milky Way Emulation mode, which models the mechanical
  304.    indexing chevron and the rotating ring.
  305.  
  306.  min_sweep: 0..7 (3 bits)
  307.    If the calculated sector sweep is less than the number of
  308.    equivalent chevron-to-chevron spans, 36 sectors is added.
  309.    Aside from the speed-dependent rumbling sound, a Milky Way
  310.    stargate whines as it accelerates and makes a little clunk
  311.    sound when acceleration is complete. Permitting short sweeps
  312.    can yield shorter dialling times though the sound effects
  313.    generator would be required to handle variable acceleration
  314.    durations and adjust the length of the whine sound and the
  315.    timing of the ring clunk sound appropriately. The limit of
  316.    7 implies a minimum sweep can be at most 28 sectors, so a
  317.    sweep from any chevron to any other chevron will be at most
  318.    7 + 8 = 15 chevrons (60 sectors) and a ring sweep from any
  319.    sector to any other sector will be at most 28 + 35 = 63
  320.    sectors, which fits nicely with the 6.22 fixed point binary
  321.    angle format used for absolute and (unsigned) relative angles.
  322.  
  323.  alternating: Boolean
  324.    Force the ring or constellation seeking to change direction
  325.    after each chevron is locked, rather than allowing the
  326.    shortest sweep (which still satisfies min_sweep) to determine
  327.    the sweep direction.
  328.  
  329.  clockwise_start: Boolean
  330.    Set True if the direction of rotation of the ring (Milky
  331.    Way gate) or roving constellation (Pegasus gate) when the
  332.    stargate begins dialling. (The starting direction cannot
  333.    be changed for an incoming wormhole.)
  334.  
  335.  skip_lit_sectors: Boolean
  336.    For Pegasus gates in Constant Angular Velocity (CAV) mode,
  337.    as indicated with acceleration = 0, there is an option to
  338.    have the roving constellation cursor cross a lit sector
  339.    (at a locked chevron) in half the usual time and thus
  340.    suppress the optical illusion of stalling.
  341.  
  342.  align_for_incoming: Boolean
  343.    Set True if a Milky Way gate should align the ring to the
  344.    home position before spinning for an incoming wormhole.
  345.    If the ring then rotates exactly 360 degrees and rests at
  346.    the home position, the indexer will activate on a sensible
  347.    constellation sector.
  348.  
  349.  start_sector_fixed: Boolean
  350.    Instantly position the ring or roving constellation when
  351.    activating the stargate. This is standard behaviour for
  352.    a Pegasus gate only.
  353.  
  354.  start_sector: uint (>= 6 bits)
  355.    The starting sector of the ring or constellation, if the
  356.    start_sector_fixed flag is set. Otherwise the ring or
  357.    cursor is left as it was after the previous operation.
  358.  
  359.  max_speed: uint (16 bits)
  360.    The upper limit of the speed in units of 1/(2^18) sectors
  361.    per millisecond. Because the kinematic maths used in this
  362.    model only permits multiples of perfect squares of
  363.    intervals in milliseconds, the top speed reached may be
  364.    slighlty less than max_speed. This is to easily allow
  365.    perfectly deterministic timings despite variable time
  366.    increments being supplied to multiple StargateState
  367.    instances while using integer arithmetic suitable for
  368.    microcontrollers.
  369.  
  370.  incoming_speed: uint (16 bits)
  371.    Incoming wormholes are indicated with a slow and steady
  372.    anticlockwise rotation of the ring on Milky Way gates and
  373.    with a sort of circular progress gauge (made of brightly
  374.    lit constellation sectors) on Pegasus gates. Because
  375.    the incoming animation is implemented here using the
  376.    sector-to-sector sweep code and uses a 16-bit time value
  377.    as for animation progress, it is important to not to set
  378.    the speed so low that the required time is unable to fit
  379.    in the progress field, which is good for about 65 seconds.
  380.  
  381.  incoming_sweep: uint (>= 6 bits)
  382.    In the Milky Way emulation mode, the constellation ring
  383.    spins steadily anticlockwise with no particular regard to
  384.    the chevrons locking in a clockwise pattern. The required
  385.    sweep angle in sectors (max 63) is set here.
  386.  
  387.  incoming_chev_delay: 0..255 (0..4080ms in 16ms steps)
  388.    In the Milky Way emulation mode, this is the time it takes
  389.    for the first chevron to begin locking during an incoming
  390.    wormhole sequence.
  391.  
  392.  incoming_chev_period: 0..255 (0..4080ms in 16ms steps)
  393.    In the Milky Way emulation mode, this is the interval
  394.    between the start of  locking of successive time it takes
  395.    for the first chevron to begin locking during an incoming
  396.    wormhole sequence.
  397.  
  398.  acceleration: 0, 1..31 (>= 5 bits)
  399.    The acceleration of the ring or the roving constellation
  400.    is in lowest significant bit units of a 6.22 fixed point
  401.    binary sector angle per millisecond squared. Zero is a
  402.    special case of instant acceleration (constant velocity).
  403.  
  404.  dwell_time: 0..255 (0..4080ms in 16ms steps)
  405.    The dwell time is the time between sweeps. The dweel time
  406.    is independent of the chevron locking time, so a new sweep
  407.    may commence even before a chevron has finished locking.
  408.  
  409.  abort_dwell_time: 0..255 (0..4080ms in 16ms steps)
  410.    In the case of a misdial, there will be a pause between
  411.    the dialled constellation being indicated (but with an
  412.    inactive chevron) and the locked chevrons all being
  413.    released (with a heavy clunk or thump sound).
  414.    This value is also used for the dwell at the end of an
  415.    alignment operation on a Milky Way gate.
  416.  
  417.  chev_locking_time: 0..255 (0..4080ms in 16ms steps)
  418.    The total chevron locking time is used mainly by the finite
  419.    state machine. The detailed start times and time intervals
  420.    below are used by the renderer. The fall times for the
  421.    output control lines CLICK and CLACK are set by this value.
  422.  
  423.  chev_click_start_time: 0..255 (0..4080ms in 16ms steps)
  424.    This value represents the time between the dialling sweep
  425.    stopping and when the mechanical indexing chevron's frame
  426.    and wedge begins to separate, on Milky Way gates. Though
  427.    the motion of the indexing chevron is not modelled here,
  428.    the chevron should take from 282 and 333ms to fully open.
  429.    From chev_click_start_time to chev_locking_time, The control
  430.    line CLICK will be active. Pegasus-style gates output the
  431.    CLICK signal to cue the dull thump sound for a chevron
  432.    locking.
  433.  
  434.  chev_clack_start_time: 0..255 (0..4080ms in 16ms steps)
  435.    This value represents the time between the dialling sweep
  436.    stopping and the when the mechanical indexing chevron's
  437.    frame and wedge begins to snap closed. This value should
  438.    be greater than or equal to chev_click_start_time. From
  439.    chev_clack_start_time to chev_locking_time, The control
  440.    line CLACK will be active (only if mw_emulation is True).
  441.  
  442.  chev_warm_start_time: 0..255 (0..4080ms in 16ms steps)
  443.    This value represents the time between the dialling
  444.    sweep stopping and a newly "locked" chevron beginning
  445.    to illuminate. (On a Milky Way gate, the indexing chevron
  446.    lights at the same time as the selected chevron.)
  447.  
  448.  chev_warm_time: 0..255 (0..4080ms in 16ms steps)
  449.    This is the time it takes for a chevron (frame and wedge
  450.    lamps) to illuminate.
  451.  
  452.  chev_fade_start_time: 0..255 (0..4080ms in 16ms steps)
  453.    This value represents the time between the dialling sweep
  454.    stopping and the indexing chevron beginning to fade for
  455.    a Milky Way gate locking a non-final chevron.
  456.  
  457.  chev_fade_time: 0..255 (0..4080ms in 16ms steps)
  458.    This is the time it takes for a chevron (frame and wedge
  459.    lamps) to fade to its idle colour.
  460.  
  461.  opening_time: 0..255 (0..4080ms in 16ms steps)
  462.    During the opening phase the constellations between the
  463.    lit constellations at their respective chevrons (on a
  464.    Pegasus gate) brighten until the constellation ring is
  465.    uniformly lit. (This avoids the naff appearance of only
  466.    seven lit constellations on a ring on a stargate which
  467.    is sometimes fully shown, as when floating in space.)
  468.  
  469.  closing_time: 0..255 (0..4080ms in 16ms steps)
  470.    There is a brief pause between a signal to close the
  471.    portal and the chevron lamps beginning to extinguish.
  472.    The actual closing visual effects, signalled by the
  473.    OPENED control line going to the inactive state, may
  474.    last longer.
  475.  """
  476.  
  477.   def set_canonical_style(self, style):
  478.     """Set timings and behaviour according to standard style.
  479.  
  480.    The canonical or "base" styles should not be modified
  481.    willy-nilly. The timings are calculated from frame-by-frame
  482.    analysis of stargate activation sequences found on YouTube.
  483.  
  484.    See the set_style method.
  485.    """
  486.     self.style = style
  487.     if style == SgStyle.MilkyWay:
  488.       self.base_style = SgStyle.MilkyWay
  489.       self.mw_emulation = True
  490.       self.min_sweep = 4  # May be 0 to 7 chevrons
  491.       self.alternating = True
  492.       self.clockwise_start = True
  493.       self.skip_lit_sectors = False  # Pegasus CAV mode only
  494.       self.align_for_incoming = True
  495.       self.start_sector_fixed = False
  496.       self.start_sector = 0
  497.       self.max_speed = 0x900000 // (5000)
  498.       self.incoming_speed = 0x900000 // (14000)
  499.       self.incoming_sweep = 36  # Milky Way only
  500.       self.incoming_chev_delay = (1900) >> 4  # Milky Way only
  501.       self.incoming_chev_period = (1550) >> 4  # Milky Way only
  502.       self.acceleration = 3
  503.       self.dwell_time = (2917) >> 4
  504.       self.abort_dwell_time = (750) >> 4
  505.       self.chev_locking_time = (2083) >> 4
  506.       self.chev_click_start_time = (583) >> 4
  507.       self.chev_clack_start_time = (1500) >> 4  # Milky Way only
  508.       self.chev_warm_start_time = (1000) >> 4
  509.       self.chev_warm_time = (250) >> 4
  510.       self.chev_fade_start_time = (1792) >> 4
  511.       self.chev_fade_time = (292) >> 4
  512.       self.opening_time = (1042) >> 4
  513.       self.closing_time = (625) >> 4
  514.     else:
  515.       # Pegasus is the default.
  516.       self.base_style = SgStyle.Pegasus
  517.       self.mw_emulation = False
  518.       self.min_sweep = 4  # May be 0 to 7 chevrons
  519.       self.alternating = True
  520.       self.clockwise_start = False
  521.       self.skip_lit_sectors = True  # Pegasus CAV mode only
  522.       self.align_for_incoming = False
  523.       self.start_sector_fixed = True
  524.       self.start_sector = 1
  525.       self.max_speed = 0x900000 // 3000
  526.       self.incoming_speed = 0x900000 // 5555
  527.       self.incoming_sweep = 36  # Milky Way only
  528.       self.acceleration = 0
  529.       self.dwell_time = (250) >> 4
  530.       self.abort_dwell_time = (500) >> 4
  531.       self.chev_locking_time = (292) >> 4
  532.       self.chev_click_start_time = (0) >> 4
  533.       self.chev_clack_start_time = (999) >> 4 # Milky Way only
  534.       self.chev_warm_start_time = (0) >> 4
  535.       self.chev_warm_time = (292) >> 4
  536.       self.chev_fade_start_time = (999) >> 4
  537.       self.chev_fade_time = (292) >> 4
  538.       self.opening_time = (1042) >> 4
  539.       self.closing_time = (625) >> 4
  540.  
  541.   def set_style(self, style):
  542.     """Set the parameters for a particular kind of stargate,
  543.  
  544.    Standard and non-standard stargate variants are selected
  545.    here. Neither style nor base_style are used by the finite
  546.    state machine. The base_style field is used by the renderer
  547.    and the style field is provided for the convenience of the
  548.    main funnction.
  549.    """
  550.     if style == SgStyle.MilkyWay:
  551.       self.set_canonical_style(SgStyle.MilkyWay)
  552.       # Append modifications here.
  553.     elif style == SgStyle.MilkyWay_Fast:
  554.       self.set_canonical_style(SgStyle.MilkyWay)
  555.       self.min_sweep = 2  # May be 0 to 7 chevrons
  556.       self.max_speed = 0x900000 // (2000)
  557.       self.incoming_speed = 0x900000 // (8250)
  558.       self.incoming_chev_delay = (750) >> 4  # Milky Way only
  559.       self.incoming_chev_period = (950) >> 4  # Milky Way only
  560.       self.acceleration = 10
  561.       self.dwell_time = (800) >> 4
  562.       self.abort_dwell_time = (750) >> 4
  563.       self.chev_locking_time = (1200) >> 4
  564.       self.chev_click_start_time = (125) >> 4
  565.       self.chev_clack_start_time = (750) >> 4 # Milky Way only
  566.       self.chev_warm_start_time = (417) >> 4
  567.       self.chev_warm_time = (250) >> 4
  568.       self.chev_fade_start_time = (950) >> 4
  569.       self.chev_fade_time = (250) >> 4
  570.       self.opening_time = (1042) >> 4
  571.       self.closing_time = (625) >> 4
  572.     elif style == SgStyle.Pegasus_Accel:
  573.       self.set_canonical_style(SgStyle.Pegasus)
  574.       self.max_speed = 0x900000 // 1500
  575.       self.skip_lit_sectors = False  # Pegasus CAV mode only
  576.       self.acceleration = 8
  577.     elif style == SgStyle.Pegasus_Fast:
  578.       self.set_canonical_style(SgStyle.Pegasus)
  579.       self.min_sweep = 4  # May be 0 to 7 chevrons
  580.       self.max_speed = 0x900000 // 2000
  581.       self.incoming_speed = 0x900000 // (4500)
  582.       self.skip_lit_sectors = True  # Pegasus CAV mode only
  583.       self.acceleration = 0
  584.     else:
  585.       # Default
  586.       self.set_canonical_style(SgStyle.Pegasus)
  587.       # Append modifications here.
  588.     self.style = style
  589.     self.updated_computed_fields()
  590.  
  591.   def updated_computed_fields(self):
  592.     """Update the computed parameters.
  593.  
  594.    At present, there is only lit_chev_progress_bump, which
  595.    is used by the standard Pegasus to quickly pass over
  596.    lit constellation sectors during dialling.
  597.    """
  598.     # For the Pegasus gate in Constant Angular Velocity mode,
  599.     # there is an option to speed the roving constellation
  600.     # cursor through a constellation sector lit by a "locked"
  601.     # chevron. This suppresses the optical illusion of the
  602.     # cursor momentarily stalling when it passes a locked
  603.     # chevron. Since the animation is controlled by a time
  604.     # parameter "progress", it is convenient to store the
  605.     # bump to be added to the progress field of StargateState
  606.     # when a skip is required.
  607.     skip_angle = 0x020000  # A half-sector skip is standard.
  608.     skip_time = int(round(skip_angle / self.max_speed))
  609.     self.lit_chev_progress_bump = min(255, skip_time)
  610.  
  611.   def __init__(self):
  612.     """Create a new instance, setting parameters to a sensible default.
  613.  
  614.    See the class docstring for a detailed description of the fields
  615.    with the command "help(StargateParam)" in a python3 shell.
  616.    """
  617.     self.dial_sequence = [16, 20, 8, 3, 11, 30, 0]
  618.     self.lock_sequence = [1, 2, 3, 4, 5, 6, 7]
  619.     self.num_good_chevrons = 9
  620.     self.lit_chev_progress_bump = 0  # Computed, used only for Pegasus CAV
  621.     self.set_style(SgStyle.Pegasus)
  622.  
  623.  
  624. class StargateState():
  625.  
  626.   """Stargate finite state machine
  627.  
  628.  A StargateState instance is a compact and abstract representation
  629.  of a stargate prop or animation, fit for a modest microcontroller.
  630.  
  631.  It is useful to have two instances on the one microcontroller,
  632.  one for the visual effects and one to cue the sound effects via
  633.  control lines (and perhaps from there, a serial or network
  634.  interface.) To ensure proper synchronisation, have the video
  635.  stargate state be an exact copy of the audio stargate state, set
  636.  the (integer) progress field of the video state set to the
  637.  required sound propagation delay time and set the open_req field
  638.  on both.
  639.  
  640.  The operation of this stargate FSM is mainly controlled through the
  641.  open_req field and the incoming field, along with the StargateParam
  642.  object which should only be changed during the Idle and Off states.
  643.  Though a single StargateState instance is tolerant of hamfisted
  644.  operation of the open_req field, an ensemble of two or more
  645.  instances will need to be more carefully coordinated to avoid
  646.  one instacne stalling and being stuck in the incorrect state
  647.  because it was not quite ready. The incoming field must only be
  648.  changed while the stargate is idle (or off).
  649.  
  650.  Closing the portal on the stargate (by setting open_req to False) may
  651.  introduce a very tiny timing error, but that will be washed away by
  652.  time spent in the Idle or Off state or by use of the copy function
  653.  in the python3 copy module.
  654.  
  655.  Important fields for control
  656.  
  657.  state: SgState enum
  658.    Indicates the current major state of the finite state machine.
  659.  
  660.  incoming: Boolean
  661.    Selects incoming mode rather than dial-out mode.
  662.  
  663.  open_req: Boolean
  664.    Controls the opening and closing of the portal. When both open_req
  665.    is True and the stargate is ready, either the dialling sequence or
  666.    the incoming wormhole sequence is started. The user may set open_req
  667.    False at any time. When the stargate is ready to close the wormhole
  668.    or abort the dialling sequence, it will do so, then return to the
  669.    Idle state.
  670.  
  671.  progress: 16-bit uint
  672.    The time-based progress value may be manipulated in the Idle state
  673.    to introduce a delay in a particular StargateState instance.
  674.  
  675.  """
  676.  
  677.   def __init__(self):
  678.     self.state = SgState.Off
  679.     self.dial_seq_ix = 0    # Number of chevrons locked
  680.     self.ref_sector = 0     # Reference sector for which rel_angle = 0
  681.     self.rel_angle = 0      # Unsigned 24 bit in 6.22 format
  682.     self.speed = 0          # Current actual speed (16 bit)
  683.     self.sector_sweep = 0   # Unsigned 0..63 span in constellation sectors
  684.     self.chevs_passed = 0   # Number of chevrons passed during sweep
  685.     self.incoming = False   # Select incoming or dial-out mode
  686.     self.open_req = False   # Start the opening or closing sequences.
  687.     self.reversing = False  # Anticlockwise when True
  688.     self.sweeping = False   # Rumbling (MW) or power hum (Pegasus)
  689.     self.lurching = False   # Motor whine-clunk for MW gates
  690.     self.locking = False    # Chevron (and perhaps indexer) activating
  691.     self.accepted = False   # Influences shutdown animation
  692.     self.aborted = False    # Latches the inverted state of open_req
  693.     self.logging = False    # Debugging: Usually set for just one instance
  694.     self.progress = 0       # Main time-based animation parameter
  695.     self.chev_progress = 0  # Concurrent chevron animation parameter
  696.     self.shimmer_phase = 0  # Used subtle Pegasus chevron animation
  697.  
  698.   def log(self, message):
  699.     """Log a message to the console, if the logging flag is set.
  700.  
  701.    When multiple instances of StargateState are used, it is
  702.    helpful to enable logging for only one.
  703.    """
  704.     if self.logging:
  705.       print(message)
  706.  
  707.   def integrate_progress(self, limit, delta_ms):
  708.     """Have progress count up to a limit.
  709.  
  710.    Return a (rem_ms, finished) tuple where rem_ms is the number
  711.    of milliseconds to spare in case the progress field reached the
  712.    limit and finished is True iff the counting is complete.
  713.    """
  714.     t1 = self.progress + delta_ms
  715.     if t1 >= limit:
  716.       #self.locking = False
  717.       self.progress = limit
  718.       return t1 - limit, True
  719.     else:
  720.       self.progress = t1
  721.       return 0, False
  722.  
  723.   def integrate_countdown(self, delta_ms):
  724.     """Have progress count down and return unused delta time."""
  725.     rem_ms = delta_ms
  726.     if self.progress >= rem_ms:
  727.       self.progress -= rem_ms
  728.       rem_ms = 0
  729.     else:
  730.       rem_ms -= self.progress
  731.       self.progress = 0
  732.     return rem_ms
  733.  
  734.   def integrate_chev_progress(self, limit, delta_ms):
  735.     """Have chev_progress count up to a limit.
  736.  
  737.    Return a (rem_ms, finished) tuple where rem_ms is the number
  738.    of milliseconds to spare in case chev_progress reached the
  739.    limit and finished is True iff the counting is complete.
  740.    """
  741.     t1 = self.chev_progress + delta_ms
  742.     if t1 >= limit:
  743.       #self.locking = False
  744.       self.chev_progress = limit
  745.       return t1 - limit, True
  746.     else:
  747.       self.chev_progress = t1
  748.       return 0, False
  749.  
  750.   def integrate_chev_countdown(self, delta_ms):
  751.     """Have chev_progress count down and return unused delta time."""
  752.     rem_ms = delta_ms
  753.     if self.chev_progress >= rem_ms:
  754.       self.chev_progress -= rem_ms
  755.       rem_ms = 0
  756.     else:
  757.       rem_ms -= self.chev_progress
  758.       self.chev_progress = 0
  759.     return rem_ms
  760.  
  761.   def update_sweep(self, sg_param):
  762.  
  763.     """Set the sector sweep according to next chevron in the sequence.
  764.  
  765.    For Pegasus gates, sweeps are from chevron to chevron. For Milky Way
  766.    emulations, sweeps are from constellation to constellations.
  767.  
  768.    If the sector_sweep field is zero, there is nothing more to dial.
  769.    """
  770.  
  771.     x = -1
  772.     if self.dial_seq_ix < len(sg_param.lock_sequence):
  773.       x = chevron_pos(sg_param.lock_sequence[self.dial_seq_ix])
  774.     if x >= 0:
  775.       if sg_param.mw_emulation:
  776.         # Milky Way style of dialling, moving the ring until the
  777.         # desired constellation sector is positioned under the
  778.         # indexing chevron at the top.
  779.         x = (x ^ 0x5) * 4 + 2  # Fallback
  780.         if self.dial_seq_ix < len(sg_param.dial_sequence):
  781.           x = sg_param.dial_sequence[self.dial_seq_ix] & 63
  782.         if x >= 36: x -= 36
  783.         # The constellation sectors are to be arranged in clockwise
  784.         # order but the ring rotation angle is increasing clockwise.
  785.         # Therefore, to bring sector n to the indexing chevron at the
  786.         # top the ring needs to be rotated -n sectors from the home
  787.         # position.
  788.         dest_sector = 36 - x
  789.         if dest_sector >= 36: dest_sector -= 36
  790.       else:
  791.         # Assume Pegasus style gate. The first desired constellation
  792.         # appears just to the right of the top chevron and hunts for
  793.         # the target chevron, where it and that chevron remains lit
  794.         # while dialling (and the sweeping) continues from there.
  795.         dest_sector = 4 * x
  796.  
  797.       sector_sweep = dest_sector - self.ref_sector
  798.       if self.reversing: sector_sweep = -sector_sweep
  799.       if sector_sweep <= 0: sector_sweep += 36
  800.       alt_sweep = 36 - sector_sweep
  801.       if alt_sweep == 0: alt_sweep = 36
  802.       if sector_sweep < 4 * sg_param.min_sweep:
  803.         sector_sweep += 36
  804.       if alt_sweep < 4 * sg_param.min_sweep:
  805.         alt_sweep += 36
  806.       if not sg_param.alternating:
  807.         if alt_sweep < sector_sweep:
  808.           sector_sweep = alt_sweep
  809.           self.reversing = not self.reversing
  810.       self.sector_sweep = sector_sweep
  811.     else:
  812.       self.sector_sweep = 0
  813.  
  814.   def integrate_sweep(self, sg_param, delta_ms):
  815.  
  816.     """Animate the sector-to-sector sweep.
  817.  
  818.    When the sweep is complete, ref_sector will be moved
  819.    to the destination sector and both rel_angle and progress
  820.    will be zeroed.
  821.  
  822.    If the sweep is completed within delta_ms (the delta time
  823.    in milliseconds), a non-zero remaining time is returned.
  824.    """
  825.  
  826.     self.lurching = False
  827.     if self.sector_sweep == 0 or not self.sweeping:
  828.       return 0
  829.  
  830.     rem_ms = 0
  831.     full_sweep = self.sector_sweep * 0x040000
  832.     peak_speed = sg_param.max_speed
  833.     if self.incoming and sg_param.incoming_speed < peak_speed:
  834.       peak_speed = sg_param.incoming_speed
  835.  
  836.     if 1 <= sg_param.acceleration <= 31:
  837.       # Acceleration applies
  838.  
  839.       ramp_time = peak_speed // sg_param.acceleration
  840.       ramp_angle2 = (sg_param.acceleration * ramp_time * ramp_time)
  841.       if ramp_angle2 >= full_sweep:
  842.         # Limit the top speed for a short sweep.
  843.         ramp_time = sqrt_int32(full_sweep // sg_param.acceleration)
  844.         ramp_angle2 = (sg_param.acceleration * ramp_time * ramp_time)
  845.       peak_speed = sg_param.acceleration * ramp_time
  846.       cav_angle = full_sweep - ramp_angle2
  847.       cav_time = (cav_angle + peak_speed - 1) // peak_speed
  848.  
  849.       t1 = self.progress + delta_ms
  850.       self.progress = t1
  851.  
  852.       if t1 < ramp_time:
  853.         # Acceleration stage
  854.         self.lurching = True
  855.         self.speed = sg_param.acceleration * t1
  856.         self.rel_angle = (sg_param.acceleration * t1 * t1) // 2
  857.       else:
  858.         td0 = ramp_time + cav_time
  859.         if t1 <= td0:
  860.           # Constant rotation rate stage
  861.           self.speed = peak_speed
  862.           self.rel_angle = ramp_angle2 // 2 + peak_speed * (t1 - ramp_time)
  863.         else:
  864.           full_time = 2 * ramp_time + cav_time
  865.           if t1 < full_time:
  866.             # Deceleration stage
  867.             ddt = t1 - td0
  868.             self.speed = max(0, peak_speed - sg_param.acceleration * ddt)
  869.             self.rel_angle = max(0, (ramp_angle2 + 2 * cav_angle
  870.                              + 2 * peak_speed * ddt
  871.                              - sg_param.acceleration * ddt * ddt) // 2)
  872.           else:
  873.             # Finished, perhaps with time to spare
  874.             self.speed = 0
  875.             self.rel_angle = full_sweep
  876.             self.progress = full_time
  877.             self.sweeping = False
  878.             rem_ms = t1 - full_time
  879.  
  880.     else:
  881.       # Instantaneous acceleration
  882.       full_time = full_sweep // peak_speed
  883.       t1 = self.progress + delta_ms
  884.       self.progress = t1
  885.       if t1 < full_time:
  886.         self.speed = peak_speed
  887.         self.rel_angle = peak_speed * t1
  888.       else:
  889.         self.sweeping = False
  890.         rem_ms = t1 - full_time
  891.  
  892.     if not self.sweeping:
  893.       self.speed = 0
  894.       x = self.ref_sector
  895.       if self.reversing:
  896.         x -= self.sector_sweep
  897.         if x < 0: x += 36
  898.       else:
  899.         x += self.sector_sweep
  900.         if x >= 36: x -= 36
  901.       self.ref_sector = x
  902.       self.rel_angle = 0
  903.       self.progress = 0
  904.  
  905.     return rem_ms
  906.  
  907.   def abort_sweep(self, sg_param):
  908.     peak_speed = sg_param.max_speed
  909.     if self.incoming:
  910.       peak_speed = sg_param.incoming_speed
  911.     if sg_param.acceleration == 0:
  912.       self.log("Abort: Constant angular velocity mode")
  913.       s1 = (peak_speed * self.progress + 0x3FFFF) >> 18
  914.       self.sector_sweep = s1
  915.     else:
  916.       ramp_time = peak_speed // sg_param.acceleration
  917.       ramp_angle2 = (sg_param.acceleration * ramp_time * ramp_time)
  918.       rs = (ramp_angle2 + 0x7FFFF) >> 19
  919.       s = self.rel_angle >> 18
  920.       if s < rs:
  921.         self.log("Abort: Acceleration stage")
  922.         self.sector_sweep = min(self.sector_sweep, (s + 1) * 2)
  923.       elif s < self.sector_sweep - rs:
  924.         self.log("Abort: CAV stage")
  925.         self.sector_sweep = min(self.sector_sweep, (s + 1) + rs)
  926.       else:
  927.         # Already braking or about to brake
  928.         self.log("Abort: Deceleration stage")
  929.  
  930.   def integrate(self, sg_param, delta_ms):
  931.     """Advance the animation of the stargate.
  932.  
  933.    sh_param is a StargateParam instance, which holds the dialling
  934.    sequence and other parameters that are to be constant while this
  935.    StargateState is in its active states.
  936.  
  937.    delta_ms is in milliseconds.
  938.    """
  939.  
  940.     rem_ms = delta_ms
  941.     count = 0
  942.  
  943.     while rem_ms > 0:
  944.  
  945.       if self.state > SgState.PreDial:
  946.         self.shimmer_phase = (self.shimmer_phase + rem_ms) & 65535
  947.  
  948.       if self.state == SgState.Off:
  949.  
  950.         rem_ms = 0
  951.  
  952.       elif self.state == SgState.Idle:
  953.  
  954.         if self.open_req and not self.aborted:
  955.           self.state = SgState.PreDial
  956.           self.log("Idle -> PreDial")
  957.         else:
  958.           rem_ms = 0
  959.  
  960.       elif self.state == SgState.PreDial:
  961.  
  962.         rem_ms = self.integrate_countdown(rem_ms)
  963.         if self.progress == 0:
  964.           self.shimmer_phase = 0
  965.           self.chev_progress = 0
  966.           self.dial_seq_ix = 0
  967.           self.chevs_passed = 0
  968.           self.lurching = True
  969.           self.speed = 0
  970.           self.rel_angle = 0
  971.           self.locking = False
  972.           self.accepted = False
  973.           if self.incoming:
  974.             self.sweeping = False
  975.             self.state = SgState.AlignForIncoming
  976.             self.log("PreDial -> AlignForIncoming")
  977.           else:
  978.             self.sweeping = True
  979.             self.reversing = not sg_param.clockwise_start
  980.             if sg_param.start_sector_fixed:
  981.               self.ref_sector = sg_param.start_sector
  982.             self.update_sweep(sg_param)
  983.             self.state = SgState.Dialling
  984.             self.log("PreDial -> Dialling")
  985.           if self.sector_sweep == 0:
  986.             self.sector_sweep = 36
  987.           self.progress = 0
  988.  
  989.       elif self.state == SgState.Dialling:
  990.  
  991.         if not self.sweeping:
  992.           rem_ms = self.integrate_countdown(rem_ms)
  993.           if self.progress == 0:
  994.             self.sweeping = True
  995.             self.log("  Dwell complete")
  996.         if self.sweeping:
  997.           if sg_param.acceleration == 0 and sg_param.skip_lit_sectors:
  998.             x = self.ref_sector
  999.             if self.reversing:
  1000.               x = -x
  1001.             x &= 3
  1002.             x = (x + (self.rel_angle >> 18)) // 4
  1003.             if x > self.chevs_passed and 4 * x < self.sector_sweep:
  1004.               self.chevs_passed += 1
  1005.               if self.reversing:
  1006.                 cc = ((self.ref_sector + 3) >> 2) - self.chevs_passed
  1007.               else:
  1008.                 cc = (self.ref_sector >> 2) + self.chevs_passed
  1009.               while cc < 0: cc += 9
  1010.               while cc >= 9: cc -= 9
  1011.               #self.log(f"  Passed chevron {cc}")
  1012.               is_lit = False
  1013.               for i in range(self.dial_seq_ix):
  1014.                 cp = chevron_pos(sg_param.lock_sequence[i])
  1015.                 if cp == cc:
  1016.                   is_lit = True
  1017.                   break
  1018.               if is_lit:
  1019.                 #self.log(f"  Passed locked chevron {cc}")
  1020.                 self.progress += sg_param.lit_chev_progress_bump
  1021.               else:
  1022.                 #self.log(f"  Passed idle chevron {cc}")
  1023.                 pass
  1024.           rem_ms = self.integrate_sweep(sg_param, rem_ms)
  1025.           if not self.sweeping:
  1026.             self.log("  Sweep complete")
  1027.             self.sweeping = False  # Begin dwell
  1028.             self.progress = sg_param.dwell_time << 4
  1029.             self.chevs_passed = 0
  1030.             if self.dial_seq_ix < sg_param.num_good_chevrons:
  1031.               self.log("  Calculating new sweep")
  1032.               self.dial_seq_ix += 1
  1033.               if sg_param.alternating:
  1034.                 self.reversing = not self.reversing
  1035.               self.update_sweep(sg_param)
  1036.               self.rem_cav_sweep = self.sector_sweep
  1037.               self.chev_progress = 0
  1038.               self.locking = True
  1039.               if not self.sector_sweep:
  1040.                 self.accepted = True
  1041.                 self.state = SgState.FinalChevron
  1042.                 self.log("Dialling -> FinalChevron")
  1043.                 self.progress = 0
  1044.             else:
  1045.               # Misdial: Abortion is imminent.
  1046.               self.progress = sg_param.abort_dwell_time << 4
  1047.               self.sector_sweep = 0
  1048.               self.state = SgState.Misdialled
  1049.               self.log("Misdialled! Abort soon!")
  1050.         if self.locking:
  1051.           # Except for the last chevron to be locked, chevron locking
  1052.           # operates in parallel with dwell and constellation sweeps.
  1053.           chev_rem_ms, finished = self.integrate_chev_progress(
  1054.               sg_param.chev_locking_time << 4, delta_ms)
  1055.           if finished:
  1056.             self.log("  Chevron locked!")
  1057.             self.locking = False
  1058.             self.chev_progress = 0
  1059.         else:
  1060.           if not self.open_req:
  1061.             self.aborted = True
  1062.           if self.aborted:
  1063.             self.chev_progress = 0
  1064.             if self.sweeping:
  1065.               self.abort_sweep(sg_param)
  1066.             if self.dial_seq_ix >= 1:
  1067.               self.state = SgState.Dimming
  1068.             else:
  1069.               self.state = SgState.Resetting  # Includes braking
  1070.             if self.state == SgState.Dimming:
  1071.               self.log("Dialling (aborted) -> Dimming")
  1072.             else:
  1073.               self.log("Dialling (early-aborted) -> Resetting")
  1074.  
  1075.       elif self.state == SgState.Misdialled:
  1076.  
  1077.         rem_ms = self.integrate_countdown(rem_ms)
  1078.         if self.progress == 0:
  1079.           if self.dial_seq_ix >= 1:
  1080.             self.state = SgState.Dimming
  1081.             self.log("Misdialled -> Dimming")
  1082.           else:
  1083.             self.state = SgState.Resetting
  1084.             self.log("Misdialled -> Resetting")
  1085.           self.chev_progress = 0
  1086.           self.sector_sweep = 0
  1087.           self.aborted = True
  1088.  
  1089.       elif self.state == SgState.AlignForIncoming:
  1090.  
  1091.         # self.chevs_passed is repurposed as an FSM state variable.
  1092.         if self.chevs_passed == 0:
  1093.           if sg_param.align_for_incoming and self.ref_sector != 0:
  1094.             self.sweeping = True
  1095.             if self.ref_sector < 18:
  1096.               self.reversing = True
  1097.               self.sector_sweep = self.ref_sector
  1098.               self.log(f"Go anticlockwise from sector {self.ref_sector}!")
  1099.             else:
  1100.               self.reversing = False
  1101.               self.sector_sweep = 36 - self.ref_sector
  1102.             self.log("Aligning...")
  1103.             self.log(f"Sector sweep = {self.sector_sweep}")
  1104.             self.chevs_passed = 1
  1105.           else:
  1106.             self.chevs_passed = 3
  1107.         if self.chevs_passed == 1:
  1108.           if self.sweeping:
  1109.             rem_ms = self.integrate_sweep(sg_param, rem_ms)
  1110.           if not self.sweeping:
  1111.             self.log("Aligned!")
  1112.             self.progress = sg_param.abort_dwell_time << 4
  1113.             self.chevs_passed = 2
  1114.         if self.chevs_passed == 2:
  1115.           rem_ms = self.integrate_countdown(rem_ms)
  1116.           if self.progress == 0:
  1117.             self.chevs_passed = 3
  1118.         if self.chevs_passed == 3:
  1119.           if sg_param.mw_emulation:
  1120.             self.chev_progress = sg_param.incoming_chev_delay << 4
  1121.             self.reversing = True
  1122.             self.sector_sweep = sg_param.incoming_sweep
  1123.           else:
  1124.             self.ref_sector = 1
  1125.             self.reversing = False
  1126.             self.sector_sweep = 36 - self.ref_sector
  1127.           self.sweeping = True
  1128.           self.state = SgState.Incoming
  1129.           self.chevs_passed = 0
  1130.           self.log("AlignForIncoming -> Incoming")
  1131.         if self.chevs_passed < 3:
  1132.           if not self.open_req:
  1133.             self.aborted = True
  1134.           if self.aborted:
  1135.             self.chevs_passed = 0
  1136.             if self.sweeping:
  1137.               self.abort_sweep(sg_param)
  1138.             self.state = SgState.Resetting  # Includes braking
  1139.             self.log("AlignForIncoming (aborted) -> Resetting")
  1140.  
  1141.       elif self.state == SgState.Incoming:
  1142.  
  1143.         chev_rem_ms = rem_ms
  1144.         if sg_param.mw_emulation:
  1145.           if self.sweeping:
  1146.             rem_ms = self.integrate_sweep(sg_param, rem_ms)
  1147.           if not self.locking:
  1148.             chev_rem_ms = self.integrate_chev_countdown(chev_rem_ms)
  1149.             if self.chev_progress == 0:
  1150.               if self.dial_seq_ix < 8:
  1151.                 self.dial_seq_ix += 1
  1152.                 self.locking = True
  1153.           if self.locking:
  1154.             chev_rem_ms, finished = self.integrate_chev_progress(
  1155.                 sg_param.chev_warm_time << 4, chev_rem_ms)
  1156.             if finished:
  1157.               self.log("  MW Chevron locked!")
  1158.               self.locking = False
  1159.               self.chev_progress = max(0, ((sg_param.incoming_chev_period
  1160.                   - sg_param.chev_warm_time) << 4) - chev_rem_ms)
  1161.           chev_rem_ms = self.integrate_chev_countdown(chev_rem_ms)
  1162.         else:
  1163.           if self.locking:
  1164.             chev_rem_ms, finished = self.integrate_chev_progress(
  1165.                 sg_param.chev_warm_time << 4, chev_rem_ms)
  1166.             if finished:
  1167.               self.log("  Pegasus Chevron locked!")
  1168.               self.locking = False
  1169.               self.chev_progress = 0
  1170.           if self.sweeping:
  1171.             rem_ms = self.integrate_sweep(sg_param, rem_ms)
  1172.           x = (self.ref_sector + (self.rel_angle >> 18)) >> 2
  1173.           if x >= 8: x = 8
  1174.           if self.dial_seq_ix < x:
  1175.             self.dial_seq_ix = x
  1176.             self.chev_progress = 0
  1177.             self.locking = True
  1178.         if self.sweeping and not self.locking:
  1179.           if not self.open_req:
  1180.             self.aborted = True
  1181.           if self.aborted:
  1182.             self.abort_sweep(sg_param)
  1183.             if self.dial_seq_ix >= 1:
  1184.               self.state = SgState.Dimming
  1185.             else:
  1186.               self.state = SgState.Resetting  # Includes braking
  1187.             self.chev_progress = 0
  1188.             if self.state == SgState.Dimming:
  1189.               self.log("Incoming (aborted) -> Dimming")
  1190.             else:
  1191.               self.log("Incoming (early-aborted) -> Resetting")
  1192.         if not self.sweeping:
  1193.           self.dial_seq_ix = 8
  1194.           self.locking = True
  1195.           self.accepted = True
  1196.           self.state = SgState.FinalChevron
  1197.           self.log("Incoming -> FinalChevron")
  1198.           self.chev_progress = 0
  1199.           self.progress = 0
  1200.  
  1201.       elif self.state == SgState.FinalChevron:
  1202.  
  1203.         if not self.open_req:
  1204.           self.aborted = True
  1205.         rem_ms, finished = self.integrate_chev_progress(
  1206.             sg_param.chev_locking_time << 4, delta_ms)
  1207.         if finished:
  1208.           self.locking = False
  1209.           self.chev_progress = 0
  1210.           self.progress = 0
  1211.           if self.aborted:
  1212.             self.state = SgState.Dimming
  1213.             self.log("FinalChevron -> Dimming")
  1214.           else:
  1215.             if self.accepted:
  1216.               self.state = SgState.Opening
  1217.               self.log("FinalChevron -> Opening")
  1218.  
  1219.       elif self.state == SgState.Opening:
  1220.  
  1221.         rem_ms, finished = self.integrate_progress(
  1222.             sg_param.opening_time << 4, rem_ms)
  1223.         if finished:
  1224.           self.state = SgState.Open
  1225.           self.progress = 0
  1226.           self.log("opening -> Open")
  1227.  
  1228.       elif self.state == SgState.Open:
  1229.  
  1230.         if self.open_req:
  1231.           rem_ms = 0
  1232.         else:
  1233.           self.state = SgState.Closing
  1234.           self.log("Open -> Closing")
  1235.           self.progress = 0
  1236.  
  1237.       elif self.state == SgState.Closing:
  1238.  
  1239.         rem_ms, finished = self.integrate_progress(
  1240.             sg_param.closing_time << 4, rem_ms)
  1241.         if finished:
  1242.           self.state = SgState.Dimming
  1243.           self.progress = 0
  1244.           self.chev_progress = 0
  1245.           self.log("Closing -> Dimming")
  1246.  
  1247.       elif self.state == SgState.Dimming:
  1248.  
  1249.         if self.dial_seq_ix >= 1:
  1250.           dim_rem_ms, finished = self.integrate_chev_progress(
  1251.               sg_param.chev_fade_time << 4, rem_ms)
  1252.           dim_time_taken = rem_ms - dim_rem_ms
  1253.           if finished:
  1254.             self.state = SgState.Resetting
  1255.             self.chev_progress = 0
  1256.           if self.sweeping:
  1257.             self.integrate_sweep(sg_param, dim_time_taken)
  1258.           rem_ms = dim_rem_ms
  1259.         else:
  1260.           self.state = SgState.Resetting
  1261.           self.chev_progress = 0
  1262.           self.log("Chevrons already extinguished")
  1263.         if self.state == SgState.Resetting: self.log("Dimming -> Resetting")
  1264.  
  1265.       elif self.state == SgState.Resetting:
  1266.  
  1267.         if self.sweeping:
  1268.           rem_ms = self.integrate_sweep(sg_param, rem_ms)
  1269.         if not self.sweeping:
  1270.           self.state = SgState.Idle
  1271.           self.log("Resetting -> Idle")
  1272.           self.progress = 0
  1273.           self.chev_progress = 0
  1274.           self.locking = False
  1275.           self.accepted = False
  1276.           self.dial_seq_ix = 0
  1277.  
  1278.       count += 1
  1279.       if count >= 10:
  1280.         self.log(f"Hung on state: {self.state}")
  1281.         break
  1282.  
  1283.     # while rem_ms
  1284.  
  1285.  
  1286. # Seven-segment display data
  1287.  
  1288. seven_seg_points = np.array([
  1289.   [0.0, 1.0], [1.0, 1.0], [1.0, 0.5], [1.0, 0.0], [0.0, 0.0], [0.0, 0.5],
  1290.   [1.30, 0.00],
  1291.   [1.25, 0.025], [1.35, 0.025], [1.35, -0.025], [1.25, -0.025],
  1292. ])
  1293.  
  1294. seven_seg_runs = {
  1295.   "0": ((0, 1, 3, 4, 0),),
  1296.   "1": ((1, 3),),
  1297.   "2": ((0, 1, 2, 5, 4, 3),),
  1298.   "3": ((0, 1, 3, 4), (5, 2),),
  1299.   "4": ((0, 5, 2), (1, 3),),
  1300.   "5": ((1, 0, 5, 2, 3, 4),),
  1301.   "6": ((1, 0, 4, 3, 2, 5),),
  1302.   "7": ((0, 1, 3),),
  1303.   "8": ((5, 0, 1, 3, 4, 5, 2),),
  1304.   "9": ((2, 5, 0, 1, 3, 4),),
  1305.   "-": ((2, 5),),
  1306.   ".": ((7, 8, 9, 10, 7),),
  1307. }
  1308.  
  1309.  
  1310. def draw_digit_7seg(surface, stdrect, col, ch, skew=None, segwidth=1):
  1311.   """Draw a single seven-segment character on a pygame surface.
  1312.  
  1313.  Characters may be from the set {"0".."9", "-", "."}.
  1314.  
  1315.  stdrect is a pygame rectangle indicating the extents of the
  1316.  corner vertices of an unskewed numeral zero on surface.
  1317.  """
  1318.   if skew is None: skew = 0.17632698  # tan(10 degrees)
  1319.   M = np.array([
  1320.     [stdrect.w, 0.0],
  1321.     [skew * stdrect.h, -stdrect.h],
  1322.   ])
  1323.   P = (seven_seg_points @ M) + np.array(stdrect.bottomleft)
  1324.   for run in seven_seg_runs.get(ch, ()):
  1325.     pg.draw.lines(
  1326.       surface,
  1327.       col,
  1328.       closed=False,
  1329.       points=[P[i] for i in run],
  1330.       width=segwidth,
  1331.     )
  1332.  
  1333.  
  1334. def draw_nstr_7seg(
  1335.   surface,
  1336.   leading_rect,
  1337.   col, nstr,
  1338.   skew=None,
  1339.   seg_lw=1,
  1340.   small_decimals=False,
  1341. ):
  1342.   """Draw a seven-segment decimal number on a pygame surface."""
  1343.   if skew is None: skew = 0.17632698  # tan(10 degrees)
  1344.   R = leading_rect.copy()
  1345.   for ch in nstr:
  1346.     if ch == '.':
  1347.       if small_decimals:
  1348.         s = 0.6
  1349.         R = pg.Rect((R.left, R.top), (s * R.w, s * R.h))
  1350.       R.right = R.left - 0.6 * R.width
  1351.       draw_digit_7seg(surface, R, col, ch, skew, seg_lw)
  1352.       R.left = R.right + 0.6 * R.width
  1353.     else:
  1354.       draw_digit_7seg(surface, R, col, ch, skew, seg_lw)
  1355.       R.left = R.right + 0.6 * R.width
  1356.  
  1357.  
  1358. def draw_stargate_vstate(surface, sg_param, sg_state, mwrcs=None, hud=False):
  1359.   """Draw a stargate according to the visual state,
  1360.  
  1361.  sg_param is the StargateParam object used to hold the dialling
  1362.  sequence and the behaviour and timing parameters.
  1363.  
  1364.  sg_state is the StargateState object finite state machine, the
  1365.  one intended for visual output.
  1366.  
  1367.  mwrcs is the array of Milky Way (emulation) segment colours or None.
  1368.  
  1369.  When hud is True, annotation such as the ring or cursor angle
  1370.  and the rotation speed is displayed.
  1371.  """
  1372.  
  1373.   if mwrcs is not None:
  1374.     mw_ring_colours = mwrcs
  1375.   else:
  1376.     mw_ring_colours = np.array([[150, 150, 120]] * 36)
  1377.     mw_ring_colours[[34, 35, 0, 1, 2]] = [255, 255, 255]
  1378.     mw_ring_colours[[16, 17, 18, 19, 20]] = [100, 100, 70]
  1379.  
  1380.   size = np.array([surface.get_width(), surface.get_height()])
  1381.   C = size // 2
  1382.   max_r = 0.98 * min(C[0], C[1])
  1383.   body_col = (0, 119, 221)
  1384.   cr_col = (0, 119, 221)  # Constellation ring colour
  1385.   ch_col = (0, 119, 221)  # Chevron colour (when inactive)
  1386.   lit_sector_col = (85, 221, 255)
  1387.  
  1388.   if sg_param.base_style == SgStyle.MilkyWay:
  1389.     dim_vcol = (160, 0, 0)
  1390.     dim_wcol = (255, 0, 0)
  1391.     lit_vcol = (255, 119, 0)
  1392.     lit_wcol = (255, 240, 0)
  1393.     vwarm_fn = lambda u: 1.5 * u
  1394.     wwarm_fn = lambda u: 1.14286 * (u - 0.125)
  1395.     vcool_fn = lambda u: vwarm_fn(1.0 - u)
  1396.     wcool_fn = lambda u: wwarm_fn(1.0 - u)
  1397.   else:
  1398.     dim_vcol = (0, 0, 64)
  1399.     dim_wcol = (0, 32, 128)
  1400.     lit_vcol = (0, 200, 255)
  1401.     lit_wcol = (0, 255, 255)
  1402.     vwarm_fn = lambda u: 3.6 * (u - 0.0)
  1403.     wwarm_fn = lambda u: 1.5 * (u - 0.3333)
  1404.     vcool_fn = lambda u: 1 - (7.0 * (u - 0.125))
  1405.     wcool_fn = lambda u: 1 - u
  1406.   default_fade_fn = lambda u: ((6 * u) // 1) & 1
  1407.   vfader = 0.0
  1408.   wfader = 0.0
  1409.   fade_v = False
  1410.   fade_w = False
  1411.   shimmer_col = (75, 185, 255)
  1412.  
  1413.   full_circle = 36 * 0x040000
  1414.   abs_angle = sg_state.ref_sector * 0x040000
  1415.   if sg_state.reversing:
  1416.     abs_angle -= sg_state.rel_angle
  1417.     if abs_angle < 0: abs_angle += full_circle
  1418.   else:
  1419.     abs_angle += sg_state.rel_angle
  1420.     if abs_angle >= full_circle: abs_angle -= full_circle
  1421.  
  1422.   # Ring
  1423.  
  1424.   ibr = 0.73 * max_r    # Inner body radius
  1425.   icrr = 0.75 * max_r   # Inner constellation ring radius
  1426.   ocrr = 0.86 * max_r   # Outer constellation ring radius
  1427.   obr = 0.96 * max_r    # Outer body radius
  1428.  
  1429.   pg.draw.circle(surface, body_col, C, ibr, 1)
  1430.   pg.draw.circle(surface, body_col, C, obr, 1)
  1431.   pg.draw.circle(surface, cr_col, C, icrr, 1)
  1432.   pg.draw.circle(surface, cr_col, C, ocrr, 1)
  1433.  
  1434.   # Ring metrics
  1435.  
  1436.   r0 = icrr - 0.07 * max_r  # Inner extent of sector tick
  1437.   r1 = icrr - 0.04 * max_r  # Outer extent of sector tick
  1438.   hsa = (2 * np.pi / 36) / 2  # Half sector angle in radians
  1439.   crr = icrr + 0.5 * (ocrr - icrr) # Constellation ring radius
  1440.   cr = 0.3 * (ocrr - icrr)  # Constellation marker radius
  1441.  
  1442.   # Dialling state
  1443.  
  1444.   if sg_state.reversing:
  1445.     dialling_aix = sg_state.ref_sector - (sg_state.rel_angle >> 18)
  1446.     if dialling_aix < 0: dialling_aix += 36
  1447.   else:
  1448.     dialling_aix = sg_state.ref_sector + (sg_state.rel_angle >> 18)
  1449.     if dialling_aix >= 36: dialling_aix -= 36
  1450.  
  1451.   # Locked chevron positions
  1452.   if sg_state.incoming:
  1453.     lcps = [(1 + i) % 9 for i in range(sg_state.dial_seq_ix)]
  1454.   else:
  1455.     n = min(sg_state.dial_seq_ix, len(sg_param.lock_sequence))
  1456.     lcps = [chevron_pos(sg_param.lock_sequence[i])
  1457.         for i in range(n)]
  1458.   acp = -1  # Active chevron position (-1 means none)
  1459.  
  1460.   if sg_state.state in (
  1461.     SgState.Dialling,
  1462.     SgState.Incoming,
  1463.     SgState.FinalChevron
  1464.   ):
  1465.     if sg_state.dial_seq_ix >= 1:
  1466.       if sg_state.locking:
  1467.         if sg_state.incoming:
  1468.           acp = sg_state.dial_seq_ix
  1469.           if sg_state.state == SgState.FinalChevron:
  1470.             acp = 0
  1471.         else:
  1472.           x = sg_state.dial_seq_ix - 1
  1473.           if x < len(sg_param.lock_sequence):
  1474.             if sg_param.lock_sequence[x]:
  1475.               acp = chevron_pos(sg_param.lock_sequence[x])
  1476.  
  1477.   # Constellations
  1478.  
  1479.   for aix in range(36):
  1480.     a = aix * 2 * np.pi / 36
  1481.     # Division line
  1482.     R = np.array([np.sin(a - hsa), -np.cos(a - hsa)])
  1483.     pg.draw.line(surface, cr_col, C + icrr * R, C + ocrr * R, 1)
  1484.     R = np.array([np.sin(a), -np.cos(a)])
  1485.     if hud:
  1486.     # Inner tick
  1487.       pg.draw.line(surface, cr_col, C + r0 * R, C + r1 * R, 1)
  1488.     # Constellation
  1489.     col = (0, 0, 0)
  1490.     lw = 1
  1491.     if sg_param.mw_emulation:
  1492.       # The ring displays all constellations and rotates the desired
  1493.       # constellations to the indexing chevron at the top.
  1494.       if sg_state.state >= SgState.Idle:
  1495.         col = ring_colour_at(aix * 0x040000 - abs_angle, mw_ring_colours)
  1496.         lw = 2
  1497.     else:
  1498.       # The ring is blank except for the roving constellation and the
  1499.       # constellations already brought to their (locked) chevrons,
  1500.       if sg_state.state == SgState.Idle:
  1501.         col = cr_col
  1502.       elif sg_state.state in (
  1503.         SgState.Dialling,
  1504.         SgState.Misdialled,
  1505.         SgState.AlignForIncoming,
  1506.         SgState.FinalChevron
  1507.       ):
  1508.         if SgState.AlignForIncoming:
  1509.           col = cr_col
  1510.           lw = 2
  1511.         if dialling_aix == aix:
  1512.           col = lit_sector_col
  1513.           lw = 2
  1514.         if aix & 3 == 0 and aix // 4 in lcps:
  1515.           col = lit_sector_col
  1516.           lw = 2
  1517.         if sg_state.state == SgState.FinalChevron:
  1518.           if sg_state.incoming:
  1519.             col = lit_sector_col
  1520.             lw = 2
  1521.       elif sg_state.state == SgState.Incoming:
  1522.         x = abs_angle >> 18
  1523.         if 1 <= aix <= x or sg_state.dial_seq_ix >= 9:
  1524.           col = lit_sector_col
  1525.           lw = 2
  1526.         else:
  1527.           col = cr_col
  1528.       elif sg_state.state == SgState.Opening:
  1529.         if sg_state.incoming:
  1530.           col = lit_sector_col
  1531.           lw = 2
  1532.         else:
  1533.           if aix & 3 == 0 and aix // 4 in lcps:
  1534.             col = lit_sector_col
  1535.             lw = 2
  1536.           else:
  1537.             if sg_param.opening_time > 0:
  1538.               t = sg_state.progress / (sg_param.opening_time << 4)
  1539.             else:
  1540.               t = 1.0
  1541.             t1 = max(0, min(1, 1.5 * (t - 0.333)))
  1542.             col = t1 * np.array(lit_sector_col)
  1543.             lw = 2
  1544.       elif sg_state.state == SgState.Open:
  1545.         col = lit_sector_col
  1546.         lw = 2
  1547.       elif sg_state.state == SgState.Closing:
  1548.         col = cr_col
  1549.         lw = 1
  1550.       elif sg_state.state == SgState.Dimming:
  1551.         if sg_state.accepted:
  1552.           col = cr_col
  1553.         lw = 1
  1554.     if sum(col) > 0:
  1555.       M = np.array([[R[0], R[1]], [-R[1], R[0]]])
  1556.       pg.draw.circle(surface, col, C + [crr, 0] @ M, cr, lw)
  1557.  
  1558.   # Chevrons
  1559.  
  1560.   wx0 = icrr + 1.00 * (ocrr - icrr)
  1561.   wx1 = obr * 1.025
  1562.   wy0 = 0.010 * obr
  1563.   wy1 = 0.065 * obr
  1564.  
  1565.   Wedge = np.array([
  1566.     [wx1, wy1],
  1567.     [wx0, wy0],
  1568.     [wx0, -wy0],
  1569.     [wx1, -wy1],
  1570.   ])
  1571.  
  1572.   vx0 = wx0 - 0.010 * obr
  1573.   vx1 = wx1 - 0.021 * obr
  1574.   vx2 = vx1
  1575.   vx3 = vx0 - 0.020 * obr
  1576.   vy0 = 0.018 * obr
  1577.   vy1 = 0.068 * obr
  1578.   vy2 = 0.12 * obr
  1579.   vy3 = 0.020 * obr
  1580.   vx4 = vx0 + 0.2 * (vx3 - vx0)
  1581.  
  1582.   sx0 = vx1
  1583.   sx1 = sx0
  1584.   sx2 = sx0 + 0.005 * obr
  1585.   sx3 = sx0 + 0.80 * (wx1 - sx0)
  1586.   sx4 = wx1
  1587.   sy0 = wy1 - (wx1 - vx1)*(wy1 - wy0)/(wx1 - wx0)
  1588.   sy1 = sy0 + 0.17 * obr
  1589.   sy2 = sy1
  1590.   sy3 = sy0 + 0.5 * (sy1 - sy0)
  1591.   sy4 = wy1
  1592.  
  1593.   Frame = np.array([
  1594.     [vx1, vy1],
  1595.     [vx0, vy0],
  1596.     [vx0, -vy0],
  1597.     [vx1, -vy1],
  1598.     [vx2, -vy2],
  1599.     [vx3, -vy3],
  1600.     [vx3, vy3],
  1601.     [vx2, vy2],
  1602.   ])
  1603.  
  1604.   Indexer = np.array([
  1605.     [vx1, vy1],
  1606.     [vx0, vy0],
  1607.     [vx0, -vy0],
  1608.     [vx1, -vy1],
  1609.     [vx2, -vy2],
  1610.     [vx3, -vy3],
  1611.     [vx4, -vy3],
  1612.     [vx4, vy3],
  1613.     [vx3, vy3],
  1614.     [vx2, vy2],
  1615.   ])
  1616.  
  1617.   Shoulder = np.array([
  1618.     [sx0, sy0],
  1619.     [sx1, sy1],
  1620.     [sx2, sy2],
  1621.     [sx3, sy3],
  1622.     [sx4, sy4],
  1623.   ])
  1624.  
  1625.   for i in range(9):
  1626.  
  1627.     shimmer_phase = 0.0
  1628.     do_shimmer = False
  1629.  
  1630.     a = np.pi * (-0.5 + (i - 0) * 2 / 9)
  1631.     R = np.array([np.cos(a), np.sin(a)])
  1632.     M = np.array([[R[0], R[1]], [-R[1], R[0]]])
  1633.     wcol = vcol = ch_col
  1634.     vlw = wlw = 1
  1635.     activating = False
  1636.  
  1637.     D = np.array([0, 0])
  1638.     F = Frame
  1639.  
  1640.     vfade_fn = default_fade_fn
  1641.     wfade_fn = default_fade_fn
  1642.     vfader = 0.0
  1643.     wfader = 0.0
  1644.     fade_v = False
  1645.     fade_w = False
  1646.  
  1647.     if sg_param.base_style == SgStyle.MilkyWay:
  1648.  
  1649.       S = Shoulder
  1650.       pg.draw.lines(surface, ch_col, False, C + D + S @ M, width=1)
  1651.       S = Shoulder @ np.array([[1, 0], [0, -1]])
  1652.       pg.draw.lines(surface, ch_col, False, C + D + S @ M, width=1)
  1653.  
  1654.       if sg_state.state in (
  1655.         SgState.Dialling,
  1656.         SgState.Misdialled,
  1657.         SgState.FinalChevron,
  1658.         SgState.Incoming,
  1659.         SgState.Opening,
  1660.         SgState.Open,
  1661.         SgState.Closing
  1662.       ):
  1663.         if i in lcps:
  1664.           if i == acp:
  1665.             activating = True
  1666.         else:
  1667.           activating = sg_state.state == SgState.FinalChevron
  1668.         if (not activating and (i in lcps or sg_state.state in
  1669.             (SgState.Opening, SgState.Open, SgState.Closing))):
  1670.           vlw = wlw = 2
  1671.           vcol = lit_vcol
  1672.           wcol = lit_wcol
  1673.         if sg_state.state == SgState.Incoming:
  1674.           if activating:
  1675.             u = sg_state.chev_progress / (sg_param.chev_warm_time << 4)
  1676.             vfader = wfader = u
  1677.             vfade_fn = vwarm_fn
  1678.             wfade_fn = wwarm_fn
  1679.             fade_v = fade_w = True
  1680.         else:
  1681.           if activating or (i == 0):
  1682.             t = (sg_state.chev_progress
  1683.                 - (sg_param.chev_warm_start_time << 4))
  1684.             if t >= 0:
  1685.               u = t / (sg_param.chev_warm_time << 4)
  1686.               vfader = wfader = u
  1687.               vfade_fn = vwarm_fn
  1688.               wfade_fn = wwarm_fn
  1689.               fade_v = fade_w = True
  1690.       if sg_state.state == SgState.Dimming:
  1691.         if sg_state.accepted or i in lcps:
  1692.           u = sg_state.chev_progress / (sg_param.chev_fade_time << 4)
  1693.           vfader = wfader = u
  1694.           vfade_fn = vcool_fn
  1695.           wfade_fn = wcool_fn
  1696.           fade_v = fade_w = True
  1697.       if i == 0:
  1698.         F = Indexer
  1699.         if (sg_state.locking and (sg_state.state == SgState.FinalChevron
  1700.             or sg_state.state != SgState.Incoming)):
  1701.           ct = sg_param.chev_locking_time << 4
  1702.           # Open and close the indexing chevron.
  1703.           if ct > 0:
  1704.             x = sg_param.chev_clack_start_time << 4
  1705.             s = 1  # Clack (out)
  1706.             if sg_state.chev_progress < x:
  1707.               x = sg_param.chev_click_start_time << 4
  1708.               s = 0  # Click (in)
  1709.             u = 7.14 * (sg_state.chev_progress - x) / ct
  1710.             u = max(0.0, min(1.0, u))
  1711.             if s: u = 1.0 - u
  1712.             D[1] = u * 0.015 * max_r
  1713.           # Illumination (dimming, here)
  1714.           if sg_state.state != SgState.FinalChevron:
  1715.             t = (sg_state.chev_progress
  1716.                 - (sg_param.chev_fade_start_time << 4))
  1717.             if t >= 0:
  1718.               vlw = wlw = 1
  1719.               wcol = vcol = ch_col
  1720.               fade_v = fade_w = False
  1721.               if t < sg_param.chev_fade_time << 4:
  1722.                 u = t / (sg_param.chev_fade_time << 4)
  1723.                 vfader = wfader = u
  1724.                 vfade_fn = vcool_fn
  1725.                 wfade_fn = wcool_fn
  1726.                 fade_v = fade_w = True
  1727.  
  1728.     elif sg_param.base_style == SgStyle.Pegasus:
  1729.  
  1730.       if sg_state.state in (
  1731.         SgState.Dialling,
  1732.         SgState.Misdialled,
  1733.         SgState.FinalChevron,
  1734.         SgState.Incoming,
  1735.         SgState.Opening,
  1736.         SgState.Open,
  1737.         SgState.Closing
  1738.       ):
  1739.         if sg_state.state in (
  1740.           SgState.Dialling,
  1741.           SgState.Misdialled,
  1742.           SgState.FinalChevron,
  1743.           SgState.Incoming
  1744.         ):
  1745.           do_shimmer = True
  1746.           u = 40 * ((sg_state.shimmer_phase & 65535) / 65536)
  1747.           shimmer_phase = u % 1.0
  1748.           if sg_state.incoming and shimmer_phase != 0.0:
  1749.             shimmer_phase = 1.0 - shimmer_phase
  1750.         if i in lcps:
  1751.           if i == acp:
  1752.             activating = True
  1753.         else:
  1754.           activating = sg_state.state == SgState.FinalChevron
  1755.         if (activating or i in lcps or sg_state.state in
  1756.             (SgState.Opening, SgState.Open, SgState.Closing)):
  1757.           vfader = wfader = 1
  1758.           vfade_fn = vwarm_fn
  1759.           wfade_fn = wwarm_fn
  1760.           fade_v = fade_w = True
  1761.         if activating:
  1762.           u = 0.0
  1763.           t0 = 0
  1764.           if sg_state.state != SgState.Incoming:
  1765.             t0 = sg_param.chev_warm_start_time << 4
  1766.           t = sg_state.chev_progress - t0
  1767.           if 0 <= t < (sg_param.chev_warm_time << 4):
  1768.             u = t / (sg_param.chev_warm_time << 4)
  1769.             vfader = wfader = u
  1770.             vfade_fn = vwarm_fn
  1771.             wfade_fn = wwarm_fn
  1772.             fade_v = fade_w = True
  1773.       if sg_state.state == SgState.Dimming:
  1774.         if sg_state.accepted or i in lcps:
  1775.           u = sg_state.chev_progress / (sg_param.chev_fade_time << 4)
  1776.           vfader = wfader = u
  1777.           vfade_fn = vcool_fn
  1778.           wfade_fn = wcool_fn
  1779.           fade_v = fade_w = True
  1780.  
  1781.     else:
  1782.       # Uknown style
  1783.  
  1784.       F = Frame
  1785.       vcol = [128, 0, 255]
  1786.       wcol = [255, 0, 255]
  1787.  
  1788.     if do_shimmer:
  1789.       u = np.sin(np.sqrt(shimmer_phase * np.pi * np.pi)) ** 2
  1790.       vcol = vcol + u * (np.array(shimmer_col) - vcol)
  1791.  
  1792.     if fade_v:
  1793.       u = max(0.0, min(1.0, vfade_fn(vfader)))
  1794.       vcol = dim_vcol + u * (np.array(lit_vcol) - dim_vcol)
  1795.       vlw = 2
  1796.     if fade_w:
  1797.       u = max(0.0, min(1.0, wfade_fn(wfader)))
  1798.       wcol = dim_wcol + u * (np.array(lit_wcol) - dim_wcol)
  1799.       wlw = 2
  1800.     pg.draw.lines(surface, vcol, True, C + D + F @ M, width=vlw)
  1801.     pg.draw.lines(surface, wcol, True, C - D + Wedge @ M, width=wlw)
  1802.  
  1803.   if hud:
  1804.     # Angle
  1805.     a = abs_angle * 2 * np.pi / (36 * 0x40000)
  1806.     R = np.array([np.sin(a), -np.cos(a)])
  1807.     r0 = 0.55 * max_r
  1808.     r1 = 0.71 * max_r
  1809.     pg.draw.line(surface, (0, 255, 0), C + r0 * R, C + r1 * R, 1)
  1810.  
  1811.   if hud:
  1812.     # Dialled sector
  1813.     if sg_param.mw_emulation:
  1814.       if sg_state.state in (
  1815.         SgState.Dialling,
  1816.         SgState.Misdialled,
  1817.         SgState.FinalChevron
  1818.       ):
  1819.         ix = sg_state.dial_seq_ix
  1820.         scol = (255, 144, 0)
  1821.         if not sg_state.sweeping:
  1822.           if sg_state.state != SgState.Misdialled:
  1823.             scol = (128, 255, 0)
  1824.             ix -= 1
  1825.           else:
  1826.             scol = (128, 0, 192)
  1827.           if sg_state.state == SgState.Dialling:
  1828.             if sg_state.progress < 0.25 * (sg_param.dwell_time << 4):
  1829.               ix = -1
  1830.         if 0 <= ix < len(sg_param.dial_sequence):
  1831.           x = sg_param.dial_sequence[ix] & 63
  1832.           if x >= 36: x -= 36
  1833.           u = min(1, sg_state.progress / 125.0)
  1834.           r0 = ibr + 0.5 * (icrr - ibr)
  1835.           r0 = r0 * (0.4 + 0.6 * u)
  1836.           rect = pg.Rect((C - [r0, r0]), (2 * r0, 2 * r0))
  1837.           a = (abs_angle + x * 0x040000) * 2 * np.pi / (36 * 0x040000)
  1838.           a0 = a1 = 0.5 * np.pi - a
  1839.           a0 -= hsa * u
  1840.           a1 += hsa * u
  1841.           lw = 5 if u < 1 else 3
  1842.           pg.draw.arc(surface, scol, rect, a0, a1, lw)
  1843.  
  1844.   if hud:
  1845.     # Rotational speed (and direction)
  1846.     a = (sg_state.speed / sg_param.max_speed) * (2 * np.pi / 9)
  1847.     if sg_state.reversing: a = -a
  1848.     R = np.array([np.sin(a), -np.cos(a)])
  1849.     r0 = 0.66 * max_r
  1850.     r1 = 0.73 * max_r
  1851.     pg.draw.line(surface, (192, 240, 0), C + r0 * R, C + r1 * R, 1)
  1852.     rect = pg.Rect((C - [r0, r0]), (2 * r0, 2 * r0))
  1853.     a0 = a1 = 0.5 * np.pi
  1854.     if a < 0:
  1855.       a1 -= a
  1856.     else:
  1857.       a0 -= a
  1858.     pg.draw.arc(surface, (192, 240, 0), rect, a0, a1, 1)
  1859.  
  1860.   # Wibbly wobbly swirly thing erroneously called an "event horizon".
  1861.   if sg_state.state in (SgState.Opening, SgState.Open, SgState.Closing):
  1862.     if sg_state.state == SgState.Opening:
  1863.       u = sg_state.progress / (sg_param.opening_time << 4)
  1864.     elif sg_state.state == SgState.Closing:
  1865.       u = 1.0 - (sg_state.progress / (sg_param.closing_time << 4))
  1866.     else:
  1867.       u = 1.0
  1868.     r = u * 0.98 * ibr
  1869.     col = (255 - int(255 * u), 255 - int(127 * u), 255)
  1870.     pg.draw.circle(surface, col, C, r, 4)
  1871.  
  1872.  
  1873. def draw_stargate_astate(surface, sg_param, sg_state):
  1874.  
  1875.   """Draw auxiliary stargate data according to the audio state,
  1876.  
  1877.  sg_param is the StargateParam object used to hold the dialling
  1878.  sequence and the behaviour and timing parameters.
  1879.  
  1880.  sg_state is the StargateState object finite state machine, the
  1881.  one intended for audio output, which is usally animated ahead
  1882.  of the visual state object in order to correct for sound latency
  1883.  and propagation delay.
  1884.  """
  1885.  
  1886.   size = np.array([surface.get_width(), surface.get_height()])
  1887.   C = size // 2
  1888.   max_r = 0.98 * min(C[0], C[1])
  1889.  
  1890.   # Angle
  1891.   abs_angle = sg_state.ref_sector * 0x040000
  1892.   if sg_state.reversing:
  1893.     abs_angle -= sg_state.rel_angle
  1894.   else:
  1895.     abs_angle += sg_state.rel_angle
  1896.   a = abs_angle * 2 * np.pi / (36 * 0x40000)
  1897.   R = np.array([np.sin(a), -np.cos(a)])
  1898.   r0 = 0.57 * max_r
  1899.   r1 = 0.69 * max_r
  1900.   pg.draw.line(surface, (255, 0, 0), C + r0 * R, C + r1 * R, 1)
  1901.  
  1902.   # Rotational speed (and direction)
  1903.   a = (sg_state.speed / sg_param.max_speed) * (2 * np.pi / 9)
  1904.   if sg_state.reversing: a = -a
  1905.   R = np.array([np.sin(a), -np.cos(a)])
  1906.   r0 = 0.68 * max_r
  1907.   r1 = 0.71 * max_r
  1908.   pg.draw.line(surface, (224, 0, 96), C + r0 * R, C + r1 * R, 1)
  1909.   rect = pg.Rect((C - [r0, r0]), (2 * r0, 2 * r0))
  1910.   a0 = a1 = 0.5 * np.pi
  1911.   if a < 0:
  1912.     a1 -= a
  1913.   else:
  1914.     a0 -= a
  1915.   pg.draw.arc(surface, (224, 0, 96), rect, a0, a1, 1)
  1916.  
  1917.  
  1918. def traces_from_sg_state(sg_state, sg_param):
  1919.   """Fetch output control signals from a stargate.
  1920.  
  1921.  These are the signals that would look nice on a seven-channel
  1922.  oscilloscope.
  1923.  """
  1924.   click = False  # MWE: Indexer opens
  1925.   clack = False  # MWE: Indexer closes
  1926.   if sg_state.locking:
  1927.     if sg_state.state == SgState.Incoming:
  1928.       click = True
  1929.     else:
  1930.       t = sg_state.chev_progress
  1931.       click = t >= sg_param.chev_click_start_time << 4
  1932.       if sg_param.mw_emulation:
  1933.         if (sg_state.state == SgState.Dialling
  1934.             or sg_state.state == SgState.FinalChevron):
  1935.           clack = t >= sg_param.chev_clack_start_time << 4
  1936.   result = [
  1937.     sg_state.speed / sg_param.max_speed,
  1938.     int(sg_state.sweeping),
  1939.     int(sg_state.lurching),
  1940.     int(click),
  1941.     int(clack),
  1942.     int(sg_state.state in (SgState.Opening, SgState.Open)),
  1943.     int(sg_state.state == SgState.Dimming),
  1944.   ]
  1945.   return result
  1946.  
  1947.  
  1948. def draw_traces(surface, dpix, old_values, new_values, styles):
  1949.   """Draw a stack of traces on a scrolling oscilloscope display.
  1950.  
  1951.  dpix is the number of pixels to scroll and update.
  1952.  
  1953.  A 2-pixel margin exists at the right edge to accomodate 4-pixel
  1954.  strokes which might othwerwise be clipped.
  1955.  """
  1956.   size = np.array([surface.get_width(), surface.get_height()])
  1957.   if 1 <= dpix < size[0]:
  1958.     surface.blit(
  1959.         surface, (0, 0), pg.Rect((dpix, 0), (size[0] - dpix, size[1])))
  1960.   if dpix >= 1:
  1961.     surface.fill(0x000000,
  1962.         pg.Rect((size[0] - dpix, 0), (dpix, size[1])))
  1963.   dpix1 = min(dpix, size[0])
  1964.   n = len(new_values)
  1965.   pen_margin = 2
  1966.   field_h = size[1] // n
  1967.   stack_h = field_h * n
  1968.   ch_h = field_h * 3 // 4
  1969.   top_baseline = (size[1] - stack_h) // 2 + field_h - (field_h - ch_h) // 2
  1970.   baseline = top_baseline
  1971.   field_x1 = size[0] - pen_margin
  1972.   def_col = (255, 0, 0)
  1973.   for oldv, newv, style in zip(old_values, new_values, styles):
  1974.     lw = style.get('lw', 1)
  1975.     x0 = field_x1 - dpix1
  1976.     y0 = baseline - ch_h * oldv
  1977.     x1 = field_x1
  1978.     y1 = baseline - ch_h * newv
  1979.     col = style.get('col', def_col)
  1980.     pg.draw.line(surface, col, (x0, y0), (x1, y1), lw)
  1981.     baseline += field_h
  1982.  
  1983.  
  1984. class MainCmd (IntEnum):
  1985.   """Command values to insulate the input system from business logic."""
  1986.   Null = 0
  1987.   Power = 1
  1988.   CutPower = 2
  1989.   Open = 3
  1990.   Incoming = 4
  1991.   Close = 5
  1992.   Style = 6
  1993.   HUD = 7
  1994.  
  1995.  
  1996. def main():
  1997.   """Run the pretty stargate simulation."""
  1998.  
  1999.   print(help_msg)
  2000.  
  2001.   test_angle = 0.0
  2002.  
  2003.   pg.init()
  2004.   clock = pg.time.Clock()
  2005.  
  2006.   # Requested screen size
  2007.   rss = (960, 960)
  2008.   rss = (800, 800)
  2009.  
  2010.   window_style = 0  # FULLSCREEN
  2011.   best_depth = pg.display.mode_ok(rss, window_style, 32)
  2012.   screen = pg.display.set_mode(rss, window_style, best_depth)
  2013.   screen_size = screen.get_width(), screen.get_height()
  2014.   pg.display.set_caption("Stargate Animation Modelling.")
  2015.  
  2016.   # Oscilloscope canvases
  2017.   sw = screen_size[0] * 45 // 100
  2018.   sh = screen_size[1] * 45 // 100
  2019.   scope_rect = pg.Rect(
  2020.     ((screen_size[0] - sw) // 2, (screen_size[1] - sh) // 2),
  2021.     (sw, sh)
  2022.   )
  2023.   vscope_surface = pg.Surface(scope_rect.size, screen.get_bitsize(), screen)
  2024.   vscope_surface.fill(0x000000)
  2025.   ascope_surface = pg.Surface(scope_rect.size, screen.get_bitsize(), screen)
  2026.   ascope_surface.fill(0x000000)
  2027.   scope_time_err = 0
  2028.  
  2029.   anim_counter = 0
  2030.   max_fps = 100
  2031.   dampened_fps = max_fps
  2032.   delta_time = 1.0 / max_fps
  2033.  
  2034.   sg_param = StargateParam()
  2035.   sg_vstate = StargateState()
  2036.   sg_astate = copy.copy(sg_vstate)
  2037.  
  2038.   use_hud = True
  2039.  
  2040.   old_vvals = [0]
  2041.   old_avals = [0]
  2042.  
  2043.   delta_ms = clock.tick(max_fps)
  2044.   delta_ms = clock.tick(max_fps)
  2045.  
  2046.   visual_delay = 250
  2047.   visual_delay_countdown = 0
  2048.   visual_delay_error = 0
  2049.   print(f"Sound propagation\ndelay correction: {visual_delay}ms\n")
  2050.  
  2051.   mw_ring_colours = build_mw_ring_colours()
  2052.   do_exit = False
  2053.  
  2054.   while not do_exit:
  2055.  
  2056.     cmd = MainCmd.Null
  2057.  
  2058.     for event in pg.event.get():
  2059.       if event.type == pg.QUIT:
  2060.         do_exit = True
  2061.         print("[Quit]")
  2062.       elif event.type == pg.KEYUP and event.key == pg.K_ESCAPE:
  2063.         do_exit = True
  2064.         print("[ESC] Quit")
  2065.       elif event.type == pg.KEYUP:
  2066.         if event.key == pg.K_q:
  2067.           do_exit = True
  2068.           print("[Q] Quit")
  2069.       elif event.type == pg.KEYDOWN:
  2070.         if event.key == pg.K_p:
  2071.           print("[P] Power")
  2072.           cmd = MainCmd.Power
  2073.         if event.key == pg.K_x:
  2074.           print("[X] Cut Power")
  2075.           cmd = MainCmd.CutPower
  2076.         if event.key == pg.K_o:
  2077.           print("[O] Dial Out")
  2078.           cmd = MainCmd.Open
  2079.         if event.key == pg.K_i:
  2080.           print("[I] Incoming")
  2081.           cmd = MainCmd.Incoming
  2082.         if event.key == pg.K_c:
  2083.           print("[C] Close")
  2084.           cmd = MainCmd.Close
  2085.         if event.key == pg.K_s:
  2086.           print("[S] Style")
  2087.           cmd = MainCmd.Style
  2088.         if event.key == pg.K_h:
  2089.           print("[H] HUD")
  2090.           cmd = MainCmd.HUD
  2091.  
  2092.     if cmd == MainCmd.Power:
  2093.       if (sg_vstate.state == SgState.Off
  2094.           and sg_astate.state == SgState.Off):
  2095.         sg_vstate.state = SgState.Idle
  2096.         sg_astate = copy.copy(sg_vstate)
  2097.  
  2098.     if cmd == MainCmd.CutPower:
  2099.       sg_vstate.state = SgState.Off
  2100.       sg_astate = copy.copy(sg_vstate)
  2101.  
  2102.     if cmd == MainCmd.Style:
  2103.       if (sg_vstate.state <= SgState.Idle
  2104.           and sg_astate.state <= SgState.Idle):
  2105.         sg_param.set_style((sg_param.style + 1) % len(SgStyle))
  2106.         print(f"Style: {sg_param.style}")
  2107.  
  2108.     if cmd == MainCmd.Open:
  2109.       if (sg_vstate.state == SgState.Idle
  2110.           and sg_astate.state == SgState.Idle):
  2111.         if not sg_astate.open_req and not sg_vstate.open_req:
  2112.           sg_vstate.incoming = False
  2113.           sg_vstate.aborted = False
  2114.           sg_astate = copy.copy(sg_vstate)
  2115.           sg_astate.open_req = True
  2116.           visual_delay_countdown = visual_delay
  2117.  
  2118.     if cmd == MainCmd.Incoming:
  2119.       if (sg_vstate.state == SgState.Idle
  2120.           and sg_astate.state == SgState.Idle):
  2121.         if not sg_astate.open_req and not sg_vstate.open_req:
  2122.           sg_vstate.incoming = True
  2123.           sg_vstate.aborted = False
  2124.           sg_astate = copy.copy(sg_vstate)
  2125.           sg_astate.open_req = True
  2126.           visual_delay_countdown = visual_delay
  2127.  
  2128.     if cmd == MainCmd.Close:
  2129.       if sg_vstate.open_req and sg_astate.open_req:
  2130.         sg_astate.open_req = False
  2131.         visual_delay_countdown = visual_delay
  2132.  
  2133.     if cmd == MainCmd.HUD:
  2134.       use_hud = not use_hud
  2135.  
  2136.     sg_vstate.logging = True
  2137.     sg_astate.logging = False
  2138.  
  2139.     # Render
  2140.  
  2141.     screen.fill((0, 0, 0))
  2142.     screen_size = np.array([screen.get_width(), screen.get_height()])
  2143.     C = screen_size // 2
  2144.     max_r = 0.98 * min(C[0], C[1])
  2145.  
  2146.     mwrcs = mw_ring_colours
  2147.     draw_stargate_vstate(screen, sg_param, sg_vstate, mwrcs, hud=use_hud)
  2148.     if use_hud:
  2149.       draw_stargate_astate(screen, sg_param, sg_astate)
  2150.  
  2151.     # Floor
  2152.     R = np.array([1, 0]) * max_r
  2153.     x = max_r
  2154.     y = 0.63 * max_r
  2155.     pg.draw.line(screen, (128, 48, 0), C + [-x, y], C + [x, y], 3)
  2156.  
  2157.     # Oscilloscope
  2158.     scope_time = 5000
  2159.     a0 = dict(lw=4, col=pg.Color(96, 0, 255))
  2160.     v0 = dict(lw=1, col=pg.Color(0, 100, 160))
  2161.     a1 = dict(lw=4, col=pg.Color(255, 32, 0))
  2162.     v1 = dict(lw=1, col=pg.Color(0, 170, 0))
  2163.     a2 = dict(lw=4, col=pg.Color(200, 180, 0))
  2164.     v2 = dict(lw=1, col=pg.Color(0, 140, 240))
  2165.     a3 = dict(lw=4, col=pg.Color(200, 0, 160))
  2166.     v3 = dict(lw=1, col=pg.Color(0, 180, 140))
  2167.     astyles = [a0, a2, a2, a1, a1, a3, a1]
  2168.     vstyles = [v0, v2, v2, v1, v1, v3, v1]
  2169.     avals = traces_from_sg_state(sg_astate, sg_param)
  2170.     vvals = traces_from_sg_state(sg_vstate, sg_param)
  2171.     sw = scope_rect.width - 4
  2172.     sh = scope_rect.height
  2173.     dpix0 = delta_ms * sw / scope_time - scope_time_err
  2174.     dpix = int(round(dpix0))
  2175.     scope_time_err = dpix - dpix0
  2176.     dpix = min(sw, dpix)
  2177.     draw_traces(ascope_surface, dpix, old_avals, avals, astyles)
  2178.     draw_traces(vscope_surface, dpix, old_vvals, vvals, vstyles)
  2179.     old_vvals = vvals
  2180.     old_avals = avals
  2181.     delay_pix = int(sw * visual_delay / scope_time)
  2182.     y0, y1 = scope_rect.top, scope_rect.bottom
  2183.     x0 = scope_rect.left + sw
  2184.     x1 = x0 - delay_pix
  2185.     if use_hud:
  2186.       pg.draw.line(screen, 0x007700, (x0, y0), (x0, y1), 1)
  2187.       pg.draw.line(screen, 0x770000, (x1, y0), (x1, y1), 1)
  2188.       screen.blit(ascope_surface, scope_rect, special_flags=pg.BLEND_ADD)
  2189.       screen.blit(vscope_surface, scope_rect, special_flags=pg.BLEND_ADD)
  2190.  
  2191.     if use_hud:
  2192.       # Numeric displays
  2193.  
  2194.       w = screen.get_width()
  2195.       std_digit_width = int(round(w * 20.0 / 960.0))
  2196.  
  2197.       digit_width = std_digit_width
  2198.       digit_height = 2 * digit_width
  2199.  
  2200.       # Frames per second
  2201.       #cx = 0.01 * max_r
  2202.       #x = cx - 4 * 1.6 * digit_width
  2203.       x = 10
  2204.       fps_rect = pg.Rect((x, 10), (digit_width, digit_height))
  2205.       fps = 1.0 / delta_time
  2206.       weight = 0.1
  2207.       dampened_fps = dampened_fps + weight * (fps - dampened_fps)
  2208.       nstr = f"{dampened_fps:5.1f}"
  2209.       draw_nstr_7seg(screen, fps_rect, 0xFF00CC, nstr, seg_lw=3,
  2210.           small_decimals=True)
  2211.  
  2212.       # Style index
  2213.       sn = 1 + int(sg_param.style)
  2214.       nstr = f"{sn}"
  2215.       x = screen_size[0] - 10 - digit_width * (2 + len(nstr))
  2216.       sn_rect = pg.Rect((x, 10), (digit_width, digit_height))
  2217.       draw_nstr_7seg(screen, sn_rect, 0x0099CC, nstr, seg_lw=3),
  2218.  
  2219.       if False:
  2220.         # Test graphic to verify timing
  2221.         R = np.array([np.sin(test_angle), -np.cos(test_angle)]) * 0.71 * max_r
  2222.         pg.draw.line(screen, (255, 60, 0), C, C + R, 1)
  2223.  
  2224.     pg.display.update()
  2225.  
  2226.     # Animate and integrate
  2227.  
  2228.     delta_ms = clock.tick(max_fps)
  2229.     delta_time = delta_ms / 1000.0
  2230.  
  2231.     sg_vstate.integrate(sg_param, delta_ms)
  2232.     sg_astate.integrate(sg_param, delta_ms)
  2233.  
  2234.     if visual_delay_countdown > 0:
  2235.       if visual_delay_countdown >= delta_ms:
  2236.         visual_delay_countdown -= delta_ms
  2237.         visual_delay_error = 0
  2238.       else:
  2239.         visual_delay_error = delta_ms - visual_delay_countdown
  2240.         visual_delay_countdown = 0
  2241.       if visual_delay_countdown == 0:
  2242.         sg_vstate.open_req = sg_astate.open_req
  2243.  
  2244.     anim_counter += delta_ms
  2245.     test_angle += 2.0/60.0 * np.pi * delta_time
  2246.  
  2247.  
  2248. if __name__ == '__main__':
  2249.   main()
  2250.   print("Done!")
  2251.  
Tags: Stargate
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement