Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- """Stargate Model Testbed
- This program animates a minimalistic abstract model of a Pegasus
- stargate (with Milky Way gate emulation) to be incorporated into
- a microcontroller.
- ---
- Stargate Model Testbed
- Version: 1.0.0.0
- (c) Copyright 2023, Daniel Neville (creamygoat@gmail.com)
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
- ---
- The animation of the stargate by this program is not necessarily
- a faithful representation of the appearance of the stargate. Rather,
- the animation is to prove that the compact model contains enough
- information at any given moment for a sound generator and a renderer
- or LED driver to fully represent the state of an animating stargate.
- Proposed Input Control Lines (or commands via serial):
- Incoming
- Open
- Close
- Style (2)
- Sequence (4)
- Colour (2)
- Delay (analogue, represents sound propagation delay correction)
- Serial (for programming constellation and chevron sequences)
- Output Control Lines (for external audio and visual devices):
- Speed (Fast PWM)
- Rotating
- Lurch (if acceleration used)
- Click
- Clack (if Milky Way emulation used)
- Opened
- Clunk (all chevrons reset)
- Control lines will need to be robustly protected from ESD,
- grounding issues and wire self-inductance. (Just an 8m pice
- of wire can destroy a microcontroller.) An ideal second
- control interface would be a second microcontroller with an
- Ethernet adaptor, since network interfaces are galvanically
- isolated and well protected.
- """
- import os
- import pygame as pg
- import copy
- import numpy as np
- import numpy.linalg as la
- from enum import IntEnum
- from enum import auto
- help_msg = """
- Stargate Model Testbed
- [ESC], [Q] Quit
- [P] Power
- [X] Cut power
- [O] Dial out
- [I] Incoming
- [C] Close
- [S] Select stargate style
- [H] HUD on/off
- """
- def sqrt_int32(x):
- """Return floor of the square root of a 32-bit integer."""
- x1 = x
- c = 0
- d = 1 << (32 - 2) # Second-to-top bit set
- while d > x:
- d >>= 2
- while d:
- if x1 >= c + d:
- x1 -= c + d
- c = (c >> 1) + d
- else:
- c >>= 1
- d >>= 2
- return c
- def chevron_pos(n):
- """Return a zero-based chevron position index given a chevron number.
- The chevron numbers might not be standard. Here, Chevron 7 is at
- the top, where the click-clacking indexer is on the Milky Way gates.
- Chevrons 8 and 9 are at the lower right and left respectively. The
- remaining chevrons are arranged clockwise right from the 1 o'clock
- position (on a nine-hour clock face).
- 7
- 6 1
- 5 Chevron 2
- Numbers
- 4 3
- - - - - - - - -
- 9 8
- Chevron position indices run clockwise starting at zero from the top.
- 0
- 8 1
- Chevron
- 7 Position 2
- Indices
- 6 3
- - - - - - - - -
- 5 4
- """
- x = n
- if x < 7:
- if x == 0:
- x = -1
- elif x >= 4:
- x += 2
- else:
- if x == 7:
- x = 0
- elif x < 10:
- x -= 4
- else:
- x = -1
- return x
- # The Milky Way emulation features a rotating ring patterned
- # with slight colour variations to show rotation.
- def build_mw_ring_colours():
- contour = "999865365122474210011324467522335678"
- contour = contour[:36]
- contour += "0" * (36 - len(contour))
- x = 0.3 + 0.6 * np.array([(ord(c) - ord("0")) / 9.0 for c in contour])
- x = np.array(np.rint(255 * x))
- result = np.hstack([x, x, x])
- return result
- def ring_colour_at(angle, mw_ring_colours):
- """Return an interpolated colour at a 24-bit angle on the ring.
- This interpolation is used in Milky Way stargate emulation mode in
- order to vaguely show a rotating ring on a gate built as a Pegasus
- stargate.
- The angle ranges from 0x000000 to 0x900000. Interpreted as a 6.18
- fixed-point binary format, the (absolute) angle represents the
- number of constellation sectors clockwise from top dead centre.
- """
- a = angle
- while a < 0:
- a += 0x900000
- while a >= 0x900000:
- a -= 0x900000
- s0 = a >> 18
- s1 = s0 + 1
- if s1 >= 36: s1 = 0
- f = (a & 0x03FFFF) / 0x040000
- col0 = np.array(mw_ring_colours[s0])
- col1 = np.array(mw_ring_colours[s1])
- col = np.rint(col0 + f * (col1 - col0))
- col = np.maximum([0, 0, 0], np.minimum([255, 255, 255], col))
- col = np.array(col, dtype=np.uint8)
- return col
- class SgStyle (IntEnum):
- """Stargate style integer enum
- Two styles, Pegasus and Milky Way are base styles with accurate
- timings. Other styles are modifications of the base styles.
- A Pegasus stargate, such as the one on Atlantis is blue (sometimes
- sea-foam green) chevrons and a fixed ring of thirty-six constellation
- displays. During dialling, each constellation to be dialled roams from
- chevron to chevron, settling at the target chevron before spawning the
- next constellation. (The first constellation starts at the 1 o'clock
- position and propagates anticlockwise.) There is no indexer chevron
- but the top chevron, as with the Milky Way gate is always the last to
- be "locked" (illuminated) and is associated with the home constellation
- (which is unique to each stargate and has no addressing function, but
- is presumably used to ready the stargate's ring by bring it to the
- home position (at angle zero).
- The Milky Way stargate has red-orange chevrons and an alternating
- rotating ring of thirty-nine constellations and a moving click-clacking
- indexing chevron at the top which also functions as the last chevron to
- accept a constellation, which must always be the "home" constellation
- (which is unique to each stargate and only meaningfully represents the
- home angle position on the ring). During dialling, each selected
- constellation is brought to the indexing chevron, which briefly opens
- with a click, illuminates in synch with another chevron being locked,
- closes with a clack and (if not the final chevron), extinguishes. The
- lower chevron frame is distinct by both its movement and its four bright
- hot spots illuminating its chevron frame. The Milky Way emulation mode
- used in this model designed for a Pegasus theatre prop, uses only
- thirty-six constellations.
- """
- Pegasus = 0
- MilkyWay = 1
- Pegasus_Accel = 2
- MilkyWay_Fast = 3
- Pegasus_Fast = 4
- class SgState (IntEnum):
- """Major state number of the stargate finite state machine"""
- Off = 0
- Idle = 1
- PreDial = 2
- Dialling = 3
- Misdialled = 4
- FinalChevron = 5
- AlignForIncoming = 6
- Incoming = 7
- Opening = 8
- Open = 9
- Closing = 10
- Dimming = 11
- Resetting = 12
- class StargateParam():
- """Stargate configuration parameters
- The parameters are constant for any given style and are intended
- to be shared by StargateState instances of any given style.
- Fields
- dial_sequence = array[0..8] of 0..35
- For an emulated Milky Way gate the ring is rotated to the
- constellation sectors numbered clockwise from 0 at the home
- symbol. (This means that when constellation 3 is selected,
- say, the ring is rotated 3 sectors anticlockwise from TDC
- to an absolute angle of 33 sectors.) A proper Milky Way
- gate, not emulated here, has 39 constellation sectors.
- A Pegasus gate is supposed to have the ability to display
- any given constellation at any desired sector during the
- dialling sequence. For a prop which has constellation LEDs
- in fixed positions, the dial_sequence field has no meaning,
- since the roving constellation cursor is only able to light
- some or all of the LEDs actually present.
- lock_sequence: array[0..8] of 0..9
- The lock sequence is an array of chevron numbers (1..9)
- terminated by a zero or the array length. The sequence
- determines the order in which the chevrons are locked.
- Usually the sequence is
- [1, 2, 3, 4, 5, 6, 7],
- [1, 2, 3, 4, 5, 6, 8, 7] or
- [1, 2, 3, 4, 5, 6, 8, 9, 7].
- Non-standard ordering are permitted. This may be useful
- for a theatre prop lacking distinguishability between
- addresses. Short sequences are permitted. If me_emulation
- is True, the last chevron should be chevron number 7
- (or whichever chevron is at chevron position zero, at TDC)
- in order for the indexing chevron to animate properly.
- num_good_chevrons: 0..9
- A misdial can be induced by setting num_good_chevrons less
- than the length of the lock sequence before (which may be
- shortened with a zero entry). The "bad" chevron is indicated
- by a failure to light the chevron or activate the indexer,
- a pause, then the release of all previously locked chevrons.
- style:
- An enum for the kind and variant of stargate. The style is set
- by a call to set_canonical_style or set_style as the function
- sets the parameters accordingly, but is not used by the finite
- state machine. It is a convenience field for use by the main
- function and the renderer.
- base_style:
- An enum for the kind of stargate. Base styles allow the user
- to create many style variations that are modifications of base
- styles so only the modified parameters need be coded by the user.
- Just as with the style field, base_style is not used by the
- finite state machine. It is a convenience field for use by the
- main function and the renderer.
- mw_emulation: Boolean
- Enables Milky Way Emulation mode, which models the mechanical
- indexing chevron and the rotating ring.
- min_sweep: 0..7 (3 bits)
- If the calculated sector sweep is less than the number of
- equivalent chevron-to-chevron spans, 36 sectors is added.
- Aside from the speed-dependent rumbling sound, a Milky Way
- stargate whines as it accelerates and makes a little clunk
- sound when acceleration is complete. Permitting short sweeps
- can yield shorter dialling times though the sound effects
- generator would be required to handle variable acceleration
- durations and adjust the length of the whine sound and the
- timing of the ring clunk sound appropriately. The limit of
- 7 implies a minimum sweep can be at most 28 sectors, so a
- sweep from any chevron to any other chevron will be at most
- 7 + 8 = 15 chevrons (60 sectors) and a ring sweep from any
- sector to any other sector will be at most 28 + 35 = 63
- sectors, which fits nicely with the 6.22 fixed point binary
- angle format used for absolute and (unsigned) relative angles.
- alternating: Boolean
- Force the ring or constellation seeking to change direction
- after each chevron is locked, rather than allowing the
- shortest sweep (which still satisfies min_sweep) to determine
- the sweep direction.
- clockwise_start: Boolean
- Set True if the direction of rotation of the ring (Milky
- Way gate) or roving constellation (Pegasus gate) when the
- stargate begins dialling. (The starting direction cannot
- be changed for an incoming wormhole.)
- skip_lit_sectors: Boolean
- For Pegasus gates in Constant Angular Velocity (CAV) mode,
- as indicated with acceleration = 0, there is an option to
- have the roving constellation cursor cross a lit sector
- (at a locked chevron) in half the usual time and thus
- suppress the optical illusion of stalling.
- align_for_incoming: Boolean
- Set True if a Milky Way gate should align the ring to the
- home position before spinning for an incoming wormhole.
- If the ring then rotates exactly 360 degrees and rests at
- the home position, the indexer will activate on a sensible
- constellation sector.
- start_sector_fixed: Boolean
- Instantly position the ring or roving constellation when
- activating the stargate. This is standard behaviour for
- a Pegasus gate only.
- start_sector: uint (>= 6 bits)
- The starting sector of the ring or constellation, if the
- start_sector_fixed flag is set. Otherwise the ring or
- cursor is left as it was after the previous operation.
- max_speed: uint (16 bits)
- The upper limit of the speed in units of 1/(2^18) sectors
- per millisecond. Because the kinematic maths used in this
- model only permits multiples of perfect squares of
- intervals in milliseconds, the top speed reached may be
- slighlty less than max_speed. This is to easily allow
- perfectly deterministic timings despite variable time
- increments being supplied to multiple StargateState
- instances while using integer arithmetic suitable for
- microcontrollers.
- incoming_speed: uint (16 bits)
- Incoming wormholes are indicated with a slow and steady
- anticlockwise rotation of the ring on Milky Way gates and
- with a sort of circular progress gauge (made of brightly
- lit constellation sectors) on Pegasus gates. Because
- the incoming animation is implemented here using the
- sector-to-sector sweep code and uses a 16-bit time value
- as for animation progress, it is important to not to set
- the speed so low that the required time is unable to fit
- in the progress field, which is good for about 65 seconds.
- incoming_sweep: uint (>= 6 bits)
- In the Milky Way emulation mode, the constellation ring
- spins steadily anticlockwise with no particular regard to
- the chevrons locking in a clockwise pattern. The required
- sweep angle in sectors (max 63) is set here.
- incoming_chev_delay: 0..255 (0..4080ms in 16ms steps)
- In the Milky Way emulation mode, this is the time it takes
- for the first chevron to begin locking during an incoming
- wormhole sequence.
- incoming_chev_period: 0..255 (0..4080ms in 16ms steps)
- In the Milky Way emulation mode, this is the interval
- between the start of locking of successive time it takes
- for the first chevron to begin locking during an incoming
- wormhole sequence.
- acceleration: 0, 1..31 (>= 5 bits)
- The acceleration of the ring or the roving constellation
- is in lowest significant bit units of a 6.22 fixed point
- binary sector angle per millisecond squared. Zero is a
- special case of instant acceleration (constant velocity).
- dwell_time: 0..255 (0..4080ms in 16ms steps)
- The dwell time is the time between sweeps. The dweel time
- is independent of the chevron locking time, so a new sweep
- may commence even before a chevron has finished locking.
- abort_dwell_time: 0..255 (0..4080ms in 16ms steps)
- In the case of a misdial, there will be a pause between
- the dialled constellation being indicated (but with an
- inactive chevron) and the locked chevrons all being
- released (with a heavy clunk or thump sound).
- This value is also used for the dwell at the end of an
- alignment operation on a Milky Way gate.
- chev_locking_time: 0..255 (0..4080ms in 16ms steps)
- The total chevron locking time is used mainly by the finite
- state machine. The detailed start times and time intervals
- below are used by the renderer. The fall times for the
- output control lines CLICK and CLACK are set by this value.
- chev_click_start_time: 0..255 (0..4080ms in 16ms steps)
- This value represents the time between the dialling sweep
- stopping and when the mechanical indexing chevron's frame
- and wedge begins to separate, on Milky Way gates. Though
- the motion of the indexing chevron is not modelled here,
- the chevron should take from 282 and 333ms to fully open.
- From chev_click_start_time to chev_locking_time, The control
- line CLICK will be active. Pegasus-style gates output the
- CLICK signal to cue the dull thump sound for a chevron
- locking.
- chev_clack_start_time: 0..255 (0..4080ms in 16ms steps)
- This value represents the time between the dialling sweep
- stopping and the when the mechanical indexing chevron's
- frame and wedge begins to snap closed. This value should
- be greater than or equal to chev_click_start_time. From
- chev_clack_start_time to chev_locking_time, The control
- line CLACK will be active (only if mw_emulation is True).
- chev_warm_start_time: 0..255 (0..4080ms in 16ms steps)
- This value represents the time between the dialling
- sweep stopping and a newly "locked" chevron beginning
- to illuminate. (On a Milky Way gate, the indexing chevron
- lights at the same time as the selected chevron.)
- chev_warm_time: 0..255 (0..4080ms in 16ms steps)
- This is the time it takes for a chevron (frame and wedge
- lamps) to illuminate.
- chev_fade_start_time: 0..255 (0..4080ms in 16ms steps)
- This value represents the time between the dialling sweep
- stopping and the indexing chevron beginning to fade for
- a Milky Way gate locking a non-final chevron.
- chev_fade_time: 0..255 (0..4080ms in 16ms steps)
- This is the time it takes for a chevron (frame and wedge
- lamps) to fade to its idle colour.
- opening_time: 0..255 (0..4080ms in 16ms steps)
- During the opening phase the constellations between the
- lit constellations at their respective chevrons (on a
- Pegasus gate) brighten until the constellation ring is
- uniformly lit. (This avoids the naff appearance of only
- seven lit constellations on a ring on a stargate which
- is sometimes fully shown, as when floating in space.)
- closing_time: 0..255 (0..4080ms in 16ms steps)
- There is a brief pause between a signal to close the
- portal and the chevron lamps beginning to extinguish.
- The actual closing visual effects, signalled by the
- OPENED control line going to the inactive state, may
- last longer.
- """
- def set_canonical_style(self, style):
- """Set timings and behaviour according to standard style.
- The canonical or "base" styles should not be modified
- willy-nilly. The timings are calculated from frame-by-frame
- analysis of stargate activation sequences found on YouTube.
- See the set_style method.
- """
- self.style = style
- if style == SgStyle.MilkyWay:
- self.base_style = SgStyle.MilkyWay
- self.mw_emulation = True
- self.min_sweep = 4 # May be 0 to 7 chevrons
- self.alternating = True
- self.clockwise_start = True
- self.skip_lit_sectors = False # Pegasus CAV mode only
- self.align_for_incoming = True
- self.start_sector_fixed = False
- self.start_sector = 0
- self.max_speed = 0x900000 // (5000)
- self.incoming_speed = 0x900000 // (14000)
- self.incoming_sweep = 36 # Milky Way only
- self.incoming_chev_delay = (1900) >> 4 # Milky Way only
- self.incoming_chev_period = (1550) >> 4 # Milky Way only
- self.acceleration = 3
- self.dwell_time = (2917) >> 4
- self.abort_dwell_time = (750) >> 4
- self.chev_locking_time = (2083) >> 4
- self.chev_click_start_time = (583) >> 4
- self.chev_clack_start_time = (1500) >> 4 # Milky Way only
- self.chev_warm_start_time = (1000) >> 4
- self.chev_warm_time = (250) >> 4
- self.chev_fade_start_time = (1792) >> 4
- self.chev_fade_time = (292) >> 4
- self.opening_time = (1042) >> 4
- self.closing_time = (625) >> 4
- else:
- # Pegasus is the default.
- self.base_style = SgStyle.Pegasus
- self.mw_emulation = False
- self.min_sweep = 4 # May be 0 to 7 chevrons
- self.alternating = True
- self.clockwise_start = False
- self.skip_lit_sectors = True # Pegasus CAV mode only
- self.align_for_incoming = False
- self.start_sector_fixed = True
- self.start_sector = 1
- self.max_speed = 0x900000 // 3000
- self.incoming_speed = 0x900000 // 5555
- self.incoming_sweep = 36 # Milky Way only
- self.acceleration = 0
- self.dwell_time = (250) >> 4
- self.abort_dwell_time = (500) >> 4
- self.chev_locking_time = (292) >> 4
- self.chev_click_start_time = (0) >> 4
- self.chev_clack_start_time = (999) >> 4 # Milky Way only
- self.chev_warm_start_time = (0) >> 4
- self.chev_warm_time = (292) >> 4
- self.chev_fade_start_time = (999) >> 4
- self.chev_fade_time = (292) >> 4
- self.opening_time = (1042) >> 4
- self.closing_time = (625) >> 4
- def set_style(self, style):
- """Set the parameters for a particular kind of stargate,
- Standard and non-standard stargate variants are selected
- here. Neither style nor base_style are used by the finite
- state machine. The base_style field is used by the renderer
- and the style field is provided for the convenience of the
- main funnction.
- """
- if style == SgStyle.MilkyWay:
- self.set_canonical_style(SgStyle.MilkyWay)
- # Append modifications here.
- elif style == SgStyle.MilkyWay_Fast:
- self.set_canonical_style(SgStyle.MilkyWay)
- self.min_sweep = 2 # May be 0 to 7 chevrons
- self.max_speed = 0x900000 // (2000)
- self.incoming_speed = 0x900000 // (8250)
- self.incoming_chev_delay = (750) >> 4 # Milky Way only
- self.incoming_chev_period = (950) >> 4 # Milky Way only
- self.acceleration = 10
- self.dwell_time = (800) >> 4
- self.abort_dwell_time = (750) >> 4
- self.chev_locking_time = (1200) >> 4
- self.chev_click_start_time = (125) >> 4
- self.chev_clack_start_time = (750) >> 4 # Milky Way only
- self.chev_warm_start_time = (417) >> 4
- self.chev_warm_time = (250) >> 4
- self.chev_fade_start_time = (950) >> 4
- self.chev_fade_time = (250) >> 4
- self.opening_time = (1042) >> 4
- self.closing_time = (625) >> 4
- elif style == SgStyle.Pegasus_Accel:
- self.set_canonical_style(SgStyle.Pegasus)
- self.max_speed = 0x900000 // 1500
- self.skip_lit_sectors = False # Pegasus CAV mode only
- self.acceleration = 8
- elif style == SgStyle.Pegasus_Fast:
- self.set_canonical_style(SgStyle.Pegasus)
- self.min_sweep = 4 # May be 0 to 7 chevrons
- self.max_speed = 0x900000 // 2000
- self.incoming_speed = 0x900000 // (4500)
- self.skip_lit_sectors = True # Pegasus CAV mode only
- self.acceleration = 0
- else:
- # Default
- self.set_canonical_style(SgStyle.Pegasus)
- # Append modifications here.
- self.style = style
- self.updated_computed_fields()
- def updated_computed_fields(self):
- """Update the computed parameters.
- At present, there is only lit_chev_progress_bump, which
- is used by the standard Pegasus to quickly pass over
- lit constellation sectors during dialling.
- """
- # For the Pegasus gate in Constant Angular Velocity mode,
- # there is an option to speed the roving constellation
- # cursor through a constellation sector lit by a "locked"
- # chevron. This suppresses the optical illusion of the
- # cursor momentarily stalling when it passes a locked
- # chevron. Since the animation is controlled by a time
- # parameter "progress", it is convenient to store the
- # bump to be added to the progress field of StargateState
- # when a skip is required.
- skip_angle = 0x020000 # A half-sector skip is standard.
- skip_time = int(round(skip_angle / self.max_speed))
- self.lit_chev_progress_bump = min(255, skip_time)
- def __init__(self):
- """Create a new instance, setting parameters to a sensible default.
- See the class docstring for a detailed description of the fields
- with the command "help(StargateParam)" in a python3 shell.
- """
- self.dial_sequence = [16, 20, 8, 3, 11, 30, 0]
- self.lock_sequence = [1, 2, 3, 4, 5, 6, 7]
- self.num_good_chevrons = 9
- self.lit_chev_progress_bump = 0 # Computed, used only for Pegasus CAV
- self.set_style(SgStyle.Pegasus)
- class StargateState():
- """Stargate finite state machine
- A StargateState instance is a compact and abstract representation
- of a stargate prop or animation, fit for a modest microcontroller.
- It is useful to have two instances on the one microcontroller,
- one for the visual effects and one to cue the sound effects via
- control lines (and perhaps from there, a serial or network
- interface.) To ensure proper synchronisation, have the video
- stargate state be an exact copy of the audio stargate state, set
- the (integer) progress field of the video state set to the
- required sound propagation delay time and set the open_req field
- on both.
- The operation of this stargate FSM is mainly controlled through the
- open_req field and the incoming field, along with the StargateParam
- object which should only be changed during the Idle and Off states.
- Though a single StargateState instance is tolerant of hamfisted
- operation of the open_req field, an ensemble of two or more
- instances will need to be more carefully coordinated to avoid
- one instacne stalling and being stuck in the incorrect state
- because it was not quite ready. The incoming field must only be
- changed while the stargate is idle (or off).
- Closing the portal on the stargate (by setting open_req to False) may
- introduce a very tiny timing error, but that will be washed away by
- time spent in the Idle or Off state or by use of the copy function
- in the python3 copy module.
- Important fields for control
- state: SgState enum
- Indicates the current major state of the finite state machine.
- incoming: Boolean
- Selects incoming mode rather than dial-out mode.
- open_req: Boolean
- Controls the opening and closing of the portal. When both open_req
- is True and the stargate is ready, either the dialling sequence or
- the incoming wormhole sequence is started. The user may set open_req
- False at any time. When the stargate is ready to close the wormhole
- or abort the dialling sequence, it will do so, then return to the
- Idle state.
- progress: 16-bit uint
- The time-based progress value may be manipulated in the Idle state
- to introduce a delay in a particular StargateState instance.
- """
- def __init__(self):
- self.state = SgState.Off
- self.dial_seq_ix = 0 # Number of chevrons locked
- self.ref_sector = 0 # Reference sector for which rel_angle = 0
- self.rel_angle = 0 # Unsigned 24 bit in 6.22 format
- self.speed = 0 # Current actual speed (16 bit)
- self.sector_sweep = 0 # Unsigned 0..63 span in constellation sectors
- self.chevs_passed = 0 # Number of chevrons passed during sweep
- self.incoming = False # Select incoming or dial-out mode
- self.open_req = False # Start the opening or closing sequences.
- self.reversing = False # Anticlockwise when True
- self.sweeping = False # Rumbling (MW) or power hum (Pegasus)
- self.lurching = False # Motor whine-clunk for MW gates
- self.locking = False # Chevron (and perhaps indexer) activating
- self.accepted = False # Influences shutdown animation
- self.aborted = False # Latches the inverted state of open_req
- self.logging = False # Debugging: Usually set for just one instance
- self.progress = 0 # Main time-based animation parameter
- self.chev_progress = 0 # Concurrent chevron animation parameter
- self.shimmer_phase = 0 # Used subtle Pegasus chevron animation
- def log(self, message):
- """Log a message to the console, if the logging flag is set.
- When multiple instances of StargateState are used, it is
- helpful to enable logging for only one.
- """
- if self.logging:
- print(message)
- def integrate_progress(self, limit, delta_ms):
- """Have progress count up to a limit.
- Return a (rem_ms, finished) tuple where rem_ms is the number
- of milliseconds to spare in case the progress field reached the
- limit and finished is True iff the counting is complete.
- """
- t1 = self.progress + delta_ms
- if t1 >= limit:
- #self.locking = False
- self.progress = limit
- return t1 - limit, True
- else:
- self.progress = t1
- return 0, False
- def integrate_countdown(self, delta_ms):
- """Have progress count down and return unused delta time."""
- rem_ms = delta_ms
- if self.progress >= rem_ms:
- self.progress -= rem_ms
- rem_ms = 0
- else:
- rem_ms -= self.progress
- self.progress = 0
- return rem_ms
- def integrate_chev_progress(self, limit, delta_ms):
- """Have chev_progress count up to a limit.
- Return a (rem_ms, finished) tuple where rem_ms is the number
- of milliseconds to spare in case chev_progress reached the
- limit and finished is True iff the counting is complete.
- """
- t1 = self.chev_progress + delta_ms
- if t1 >= limit:
- #self.locking = False
- self.chev_progress = limit
- return t1 - limit, True
- else:
- self.chev_progress = t1
- return 0, False
- def integrate_chev_countdown(self, delta_ms):
- """Have chev_progress count down and return unused delta time."""
- rem_ms = delta_ms
- if self.chev_progress >= rem_ms:
- self.chev_progress -= rem_ms
- rem_ms = 0
- else:
- rem_ms -= self.chev_progress
- self.chev_progress = 0
- return rem_ms
- def update_sweep(self, sg_param):
- """Set the sector sweep according to next chevron in the sequence.
- For Pegasus gates, sweeps are from chevron to chevron. For Milky Way
- emulations, sweeps are from constellation to constellations.
- If the sector_sweep field is zero, there is nothing more to dial.
- """
- x = -1
- if self.dial_seq_ix < len(sg_param.lock_sequence):
- x = chevron_pos(sg_param.lock_sequence[self.dial_seq_ix])
- if x >= 0:
- if sg_param.mw_emulation:
- # Milky Way style of dialling, moving the ring until the
- # desired constellation sector is positioned under the
- # indexing chevron at the top.
- x = (x ^ 0x5) * 4 + 2 # Fallback
- if self.dial_seq_ix < len(sg_param.dial_sequence):
- x = sg_param.dial_sequence[self.dial_seq_ix] & 63
- if x >= 36: x -= 36
- # The constellation sectors are to be arranged in clockwise
- # order but the ring rotation angle is increasing clockwise.
- # Therefore, to bring sector n to the indexing chevron at the
- # top the ring needs to be rotated -n sectors from the home
- # position.
- dest_sector = 36 - x
- if dest_sector >= 36: dest_sector -= 36
- else:
- # Assume Pegasus style gate. The first desired constellation
- # appears just to the right of the top chevron and hunts for
- # the target chevron, where it and that chevron remains lit
- # while dialling (and the sweeping) continues from there.
- dest_sector = 4 * x
- sector_sweep = dest_sector - self.ref_sector
- if self.reversing: sector_sweep = -sector_sweep
- if sector_sweep <= 0: sector_sweep += 36
- alt_sweep = 36 - sector_sweep
- if alt_sweep == 0: alt_sweep = 36
- if sector_sweep < 4 * sg_param.min_sweep:
- sector_sweep += 36
- if alt_sweep < 4 * sg_param.min_sweep:
- alt_sweep += 36
- if not sg_param.alternating:
- if alt_sweep < sector_sweep:
- sector_sweep = alt_sweep
- self.reversing = not self.reversing
- self.sector_sweep = sector_sweep
- else:
- self.sector_sweep = 0
- def integrate_sweep(self, sg_param, delta_ms):
- """Animate the sector-to-sector sweep.
- When the sweep is complete, ref_sector will be moved
- to the destination sector and both rel_angle and progress
- will be zeroed.
- If the sweep is completed within delta_ms (the delta time
- in milliseconds), a non-zero remaining time is returned.
- """
- self.lurching = False
- if self.sector_sweep == 0 or not self.sweeping:
- return 0
- rem_ms = 0
- full_sweep = self.sector_sweep * 0x040000
- peak_speed = sg_param.max_speed
- if self.incoming and sg_param.incoming_speed < peak_speed:
- peak_speed = sg_param.incoming_speed
- if 1 <= sg_param.acceleration <= 31:
- # Acceleration applies
- ramp_time = peak_speed // sg_param.acceleration
- ramp_angle2 = (sg_param.acceleration * ramp_time * ramp_time)
- if ramp_angle2 >= full_sweep:
- # Limit the top speed for a short sweep.
- ramp_time = sqrt_int32(full_sweep // sg_param.acceleration)
- ramp_angle2 = (sg_param.acceleration * ramp_time * ramp_time)
- peak_speed = sg_param.acceleration * ramp_time
- cav_angle = full_sweep - ramp_angle2
- cav_time = (cav_angle + peak_speed - 1) // peak_speed
- t1 = self.progress + delta_ms
- self.progress = t1
- if t1 < ramp_time:
- # Acceleration stage
- self.lurching = True
- self.speed = sg_param.acceleration * t1
- self.rel_angle = (sg_param.acceleration * t1 * t1) // 2
- else:
- td0 = ramp_time + cav_time
- if t1 <= td0:
- # Constant rotation rate stage
- self.speed = peak_speed
- self.rel_angle = ramp_angle2 // 2 + peak_speed * (t1 - ramp_time)
- else:
- full_time = 2 * ramp_time + cav_time
- if t1 < full_time:
- # Deceleration stage
- ddt = t1 - td0
- self.speed = max(0, peak_speed - sg_param.acceleration * ddt)
- self.rel_angle = max(0, (ramp_angle2 + 2 * cav_angle
- + 2 * peak_speed * ddt
- - sg_param.acceleration * ddt * ddt) // 2)
- else:
- # Finished, perhaps with time to spare
- self.speed = 0
- self.rel_angle = full_sweep
- self.progress = full_time
- self.sweeping = False
- rem_ms = t1 - full_time
- else:
- # Instantaneous acceleration
- full_time = full_sweep // peak_speed
- t1 = self.progress + delta_ms
- self.progress = t1
- if t1 < full_time:
- self.speed = peak_speed
- self.rel_angle = peak_speed * t1
- else:
- self.sweeping = False
- rem_ms = t1 - full_time
- if not self.sweeping:
- self.speed = 0
- x = self.ref_sector
- if self.reversing:
- x -= self.sector_sweep
- if x < 0: x += 36
- else:
- x += self.sector_sweep
- if x >= 36: x -= 36
- self.ref_sector = x
- self.rel_angle = 0
- self.progress = 0
- return rem_ms
- def abort_sweep(self, sg_param):
- peak_speed = sg_param.max_speed
- if self.incoming:
- peak_speed = sg_param.incoming_speed
- if sg_param.acceleration == 0:
- self.log("Abort: Constant angular velocity mode")
- s1 = (peak_speed * self.progress + 0x3FFFF) >> 18
- self.sector_sweep = s1
- else:
- ramp_time = peak_speed // sg_param.acceleration
- ramp_angle2 = (sg_param.acceleration * ramp_time * ramp_time)
- rs = (ramp_angle2 + 0x7FFFF) >> 19
- s = self.rel_angle >> 18
- if s < rs:
- self.log("Abort: Acceleration stage")
- self.sector_sweep = min(self.sector_sweep, (s + 1) * 2)
- elif s < self.sector_sweep - rs:
- self.log("Abort: CAV stage")
- self.sector_sweep = min(self.sector_sweep, (s + 1) + rs)
- else:
- # Already braking or about to brake
- self.log("Abort: Deceleration stage")
- def integrate(self, sg_param, delta_ms):
- """Advance the animation of the stargate.
- sh_param is a StargateParam instance, which holds the dialling
- sequence and other parameters that are to be constant while this
- StargateState is in its active states.
- delta_ms is in milliseconds.
- """
- rem_ms = delta_ms
- count = 0
- while rem_ms > 0:
- if self.state > SgState.PreDial:
- self.shimmer_phase = (self.shimmer_phase + rem_ms) & 65535
- if self.state == SgState.Off:
- rem_ms = 0
- elif self.state == SgState.Idle:
- if self.open_req and not self.aborted:
- self.state = SgState.PreDial
- self.log("Idle -> PreDial")
- else:
- rem_ms = 0
- elif self.state == SgState.PreDial:
- rem_ms = self.integrate_countdown(rem_ms)
- if self.progress == 0:
- self.shimmer_phase = 0
- self.chev_progress = 0
- self.dial_seq_ix = 0
- self.chevs_passed = 0
- self.lurching = True
- self.speed = 0
- self.rel_angle = 0
- self.locking = False
- self.accepted = False
- if self.incoming:
- self.sweeping = False
- self.state = SgState.AlignForIncoming
- self.log("PreDial -> AlignForIncoming")
- else:
- self.sweeping = True
- self.reversing = not sg_param.clockwise_start
- if sg_param.start_sector_fixed:
- self.ref_sector = sg_param.start_sector
- self.update_sweep(sg_param)
- self.state = SgState.Dialling
- self.log("PreDial -> Dialling")
- if self.sector_sweep == 0:
- self.sector_sweep = 36
- self.progress = 0
- elif self.state == SgState.Dialling:
- if not self.sweeping:
- rem_ms = self.integrate_countdown(rem_ms)
- if self.progress == 0:
- self.sweeping = True
- self.log(" Dwell complete")
- if self.sweeping:
- if sg_param.acceleration == 0 and sg_param.skip_lit_sectors:
- x = self.ref_sector
- if self.reversing:
- x = -x
- x &= 3
- x = (x + (self.rel_angle >> 18)) // 4
- if x > self.chevs_passed and 4 * x < self.sector_sweep:
- self.chevs_passed += 1
- if self.reversing:
- cc = ((self.ref_sector + 3) >> 2) - self.chevs_passed
- else:
- cc = (self.ref_sector >> 2) + self.chevs_passed
- while cc < 0: cc += 9
- while cc >= 9: cc -= 9
- #self.log(f" Passed chevron {cc}")
- is_lit = False
- for i in range(self.dial_seq_ix):
- cp = chevron_pos(sg_param.lock_sequence[i])
- if cp == cc:
- is_lit = True
- break
- if is_lit:
- #self.log(f" Passed locked chevron {cc}")
- self.progress += sg_param.lit_chev_progress_bump
- else:
- #self.log(f" Passed idle chevron {cc}")
- pass
- rem_ms = self.integrate_sweep(sg_param, rem_ms)
- if not self.sweeping:
- self.log(" Sweep complete")
- self.sweeping = False # Begin dwell
- self.progress = sg_param.dwell_time << 4
- self.chevs_passed = 0
- if self.dial_seq_ix < sg_param.num_good_chevrons:
- self.log(" Calculating new sweep")
- self.dial_seq_ix += 1
- if sg_param.alternating:
- self.reversing = not self.reversing
- self.update_sweep(sg_param)
- self.rem_cav_sweep = self.sector_sweep
- self.chev_progress = 0
- self.locking = True
- if not self.sector_sweep:
- self.accepted = True
- self.state = SgState.FinalChevron
- self.log("Dialling -> FinalChevron")
- self.progress = 0
- else:
- # Misdial: Abortion is imminent.
- self.progress = sg_param.abort_dwell_time << 4
- self.sector_sweep = 0
- self.state = SgState.Misdialled
- self.log("Misdialled! Abort soon!")
- if self.locking:
- # Except for the last chevron to be locked, chevron locking
- # operates in parallel with dwell and constellation sweeps.
- chev_rem_ms, finished = self.integrate_chev_progress(
- sg_param.chev_locking_time << 4, delta_ms)
- if finished:
- self.log(" Chevron locked!")
- self.locking = False
- self.chev_progress = 0
- else:
- if not self.open_req:
- self.aborted = True
- if self.aborted:
- self.chev_progress = 0
- if self.sweeping:
- self.abort_sweep(sg_param)
- if self.dial_seq_ix >= 1:
- self.state = SgState.Dimming
- else:
- self.state = SgState.Resetting # Includes braking
- if self.state == SgState.Dimming:
- self.log("Dialling (aborted) -> Dimming")
- else:
- self.log("Dialling (early-aborted) -> Resetting")
- elif self.state == SgState.Misdialled:
- rem_ms = self.integrate_countdown(rem_ms)
- if self.progress == 0:
- if self.dial_seq_ix >= 1:
- self.state = SgState.Dimming
- self.log("Misdialled -> Dimming")
- else:
- self.state = SgState.Resetting
- self.log("Misdialled -> Resetting")
- self.chev_progress = 0
- self.sector_sweep = 0
- self.aborted = True
- elif self.state == SgState.AlignForIncoming:
- # self.chevs_passed is repurposed as an FSM state variable.
- if self.chevs_passed == 0:
- if sg_param.align_for_incoming and self.ref_sector != 0:
- self.sweeping = True
- if self.ref_sector < 18:
- self.reversing = True
- self.sector_sweep = self.ref_sector
- self.log(f"Go anticlockwise from sector {self.ref_sector}!")
- else:
- self.reversing = False
- self.sector_sweep = 36 - self.ref_sector
- self.log("Aligning...")
- self.log(f"Sector sweep = {self.sector_sweep}")
- self.chevs_passed = 1
- else:
- self.chevs_passed = 3
- if self.chevs_passed == 1:
- if self.sweeping:
- rem_ms = self.integrate_sweep(sg_param, rem_ms)
- if not self.sweeping:
- self.log("Aligned!")
- self.progress = sg_param.abort_dwell_time << 4
- self.chevs_passed = 2
- if self.chevs_passed == 2:
- rem_ms = self.integrate_countdown(rem_ms)
- if self.progress == 0:
- self.chevs_passed = 3
- if self.chevs_passed == 3:
- if sg_param.mw_emulation:
- self.chev_progress = sg_param.incoming_chev_delay << 4
- self.reversing = True
- self.sector_sweep = sg_param.incoming_sweep
- else:
- self.ref_sector = 1
- self.reversing = False
- self.sector_sweep = 36 - self.ref_sector
- self.sweeping = True
- self.state = SgState.Incoming
- self.chevs_passed = 0
- self.log("AlignForIncoming -> Incoming")
- if self.chevs_passed < 3:
- if not self.open_req:
- self.aborted = True
- if self.aborted:
- self.chevs_passed = 0
- if self.sweeping:
- self.abort_sweep(sg_param)
- self.state = SgState.Resetting # Includes braking
- self.log("AlignForIncoming (aborted) -> Resetting")
- elif self.state == SgState.Incoming:
- chev_rem_ms = rem_ms
- if sg_param.mw_emulation:
- if self.sweeping:
- rem_ms = self.integrate_sweep(sg_param, rem_ms)
- if not self.locking:
- chev_rem_ms = self.integrate_chev_countdown(chev_rem_ms)
- if self.chev_progress == 0:
- if self.dial_seq_ix < 8:
- self.dial_seq_ix += 1
- self.locking = True
- if self.locking:
- chev_rem_ms, finished = self.integrate_chev_progress(
- sg_param.chev_warm_time << 4, chev_rem_ms)
- if finished:
- self.log(" MW Chevron locked!")
- self.locking = False
- self.chev_progress = max(0, ((sg_param.incoming_chev_period
- - sg_param.chev_warm_time) << 4) - chev_rem_ms)
- chev_rem_ms = self.integrate_chev_countdown(chev_rem_ms)
- else:
- if self.locking:
- chev_rem_ms, finished = self.integrate_chev_progress(
- sg_param.chev_warm_time << 4, chev_rem_ms)
- if finished:
- self.log(" Pegasus Chevron locked!")
- self.locking = False
- self.chev_progress = 0
- if self.sweeping:
- rem_ms = self.integrate_sweep(sg_param, rem_ms)
- x = (self.ref_sector + (self.rel_angle >> 18)) >> 2
- if x >= 8: x = 8
- if self.dial_seq_ix < x:
- self.dial_seq_ix = x
- self.chev_progress = 0
- self.locking = True
- if self.sweeping and not self.locking:
- if not self.open_req:
- self.aborted = True
- if self.aborted:
- self.abort_sweep(sg_param)
- if self.dial_seq_ix >= 1:
- self.state = SgState.Dimming
- else:
- self.state = SgState.Resetting # Includes braking
- self.chev_progress = 0
- if self.state == SgState.Dimming:
- self.log("Incoming (aborted) -> Dimming")
- else:
- self.log("Incoming (early-aborted) -> Resetting")
- if not self.sweeping:
- self.dial_seq_ix = 8
- self.locking = True
- self.accepted = True
- self.state = SgState.FinalChevron
- self.log("Incoming -> FinalChevron")
- self.chev_progress = 0
- self.progress = 0
- elif self.state == SgState.FinalChevron:
- if not self.open_req:
- self.aborted = True
- rem_ms, finished = self.integrate_chev_progress(
- sg_param.chev_locking_time << 4, delta_ms)
- if finished:
- self.locking = False
- self.chev_progress = 0
- self.progress = 0
- if self.aborted:
- self.state = SgState.Dimming
- self.log("FinalChevron -> Dimming")
- else:
- if self.accepted:
- self.state = SgState.Opening
- self.log("FinalChevron -> Opening")
- elif self.state == SgState.Opening:
- rem_ms, finished = self.integrate_progress(
- sg_param.opening_time << 4, rem_ms)
- if finished:
- self.state = SgState.Open
- self.progress = 0
- self.log("opening -> Open")
- elif self.state == SgState.Open:
- if self.open_req:
- rem_ms = 0
- else:
- self.state = SgState.Closing
- self.log("Open -> Closing")
- self.progress = 0
- elif self.state == SgState.Closing:
- rem_ms, finished = self.integrate_progress(
- sg_param.closing_time << 4, rem_ms)
- if finished:
- self.state = SgState.Dimming
- self.progress = 0
- self.chev_progress = 0
- self.log("Closing -> Dimming")
- elif self.state == SgState.Dimming:
- if self.dial_seq_ix >= 1:
- dim_rem_ms, finished = self.integrate_chev_progress(
- sg_param.chev_fade_time << 4, rem_ms)
- dim_time_taken = rem_ms - dim_rem_ms
- if finished:
- self.state = SgState.Resetting
- self.chev_progress = 0
- if self.sweeping:
- self.integrate_sweep(sg_param, dim_time_taken)
- rem_ms = dim_rem_ms
- else:
- self.state = SgState.Resetting
- self.chev_progress = 0
- self.log("Chevrons already extinguished")
- if self.state == SgState.Resetting: self.log("Dimming -> Resetting")
- elif self.state == SgState.Resetting:
- if self.sweeping:
- rem_ms = self.integrate_sweep(sg_param, rem_ms)
- if not self.sweeping:
- self.state = SgState.Idle
- self.log("Resetting -> Idle")
- self.progress = 0
- self.chev_progress = 0
- self.locking = False
- self.accepted = False
- self.dial_seq_ix = 0
- count += 1
- if count >= 10:
- self.log(f"Hung on state: {self.state}")
- break
- # while rem_ms
- # Seven-segment display data
- seven_seg_points = np.array([
- [0.0, 1.0], [1.0, 1.0], [1.0, 0.5], [1.0, 0.0], [0.0, 0.0], [0.0, 0.5],
- [1.30, 0.00],
- [1.25, 0.025], [1.35, 0.025], [1.35, -0.025], [1.25, -0.025],
- ])
- seven_seg_runs = {
- "0": ((0, 1, 3, 4, 0),),
- "1": ((1, 3),),
- "2": ((0, 1, 2, 5, 4, 3),),
- "3": ((0, 1, 3, 4), (5, 2),),
- "4": ((0, 5, 2), (1, 3),),
- "5": ((1, 0, 5, 2, 3, 4),),
- "6": ((1, 0, 4, 3, 2, 5),),
- "7": ((0, 1, 3),),
- "8": ((5, 0, 1, 3, 4, 5, 2),),
- "9": ((2, 5, 0, 1, 3, 4),),
- "-": ((2, 5),),
- ".": ((7, 8, 9, 10, 7),),
- }
- def draw_digit_7seg(surface, stdrect, col, ch, skew=None, segwidth=1):
- """Draw a single seven-segment character on a pygame surface.
- Characters may be from the set {"0".."9", "-", "."}.
- stdrect is a pygame rectangle indicating the extents of the
- corner vertices of an unskewed numeral zero on surface.
- """
- if skew is None: skew = 0.17632698 # tan(10 degrees)
- M = np.array([
- [stdrect.w, 0.0],
- [skew * stdrect.h, -stdrect.h],
- ])
- P = (seven_seg_points @ M) + np.array(stdrect.bottomleft)
- for run in seven_seg_runs.get(ch, ()):
- pg.draw.lines(
- surface,
- col,
- closed=False,
- points=[P[i] for i in run],
- width=segwidth,
- )
- def draw_nstr_7seg(
- surface,
- leading_rect,
- col, nstr,
- skew=None,
- seg_lw=1,
- small_decimals=False,
- ):
- """Draw a seven-segment decimal number on a pygame surface."""
- if skew is None: skew = 0.17632698 # tan(10 degrees)
- R = leading_rect.copy()
- for ch in nstr:
- if ch == '.':
- if small_decimals:
- s = 0.6
- R = pg.Rect((R.left, R.top), (s * R.w, s * R.h))
- R.right = R.left - 0.6 * R.width
- draw_digit_7seg(surface, R, col, ch, skew, seg_lw)
- R.left = R.right + 0.6 * R.width
- else:
- draw_digit_7seg(surface, R, col, ch, skew, seg_lw)
- R.left = R.right + 0.6 * R.width
- def draw_stargate_vstate(surface, sg_param, sg_state, mwrcs=None, hud=False):
- """Draw a stargate according to the visual state,
- sg_param is the StargateParam object used to hold the dialling
- sequence and the behaviour and timing parameters.
- sg_state is the StargateState object finite state machine, the
- one intended for visual output.
- mwrcs is the array of Milky Way (emulation) segment colours or None.
- When hud is True, annotation such as the ring or cursor angle
- and the rotation speed is displayed.
- """
- if mwrcs is not None:
- mw_ring_colours = mwrcs
- else:
- mw_ring_colours = np.array([[150, 150, 120]] * 36)
- mw_ring_colours[[34, 35, 0, 1, 2]] = [255, 255, 255]
- mw_ring_colours[[16, 17, 18, 19, 20]] = [100, 100, 70]
- size = np.array([surface.get_width(), surface.get_height()])
- C = size // 2
- max_r = 0.98 * min(C[0], C[1])
- body_col = (0, 119, 221)
- cr_col = (0, 119, 221) # Constellation ring colour
- ch_col = (0, 119, 221) # Chevron colour (when inactive)
- lit_sector_col = (85, 221, 255)
- if sg_param.base_style == SgStyle.MilkyWay:
- dim_vcol = (160, 0, 0)
- dim_wcol = (255, 0, 0)
- lit_vcol = (255, 119, 0)
- lit_wcol = (255, 240, 0)
- vwarm_fn = lambda u: 1.5 * u
- wwarm_fn = lambda u: 1.14286 * (u - 0.125)
- vcool_fn = lambda u: vwarm_fn(1.0 - u)
- wcool_fn = lambda u: wwarm_fn(1.0 - u)
- else:
- dim_vcol = (0, 0, 64)
- dim_wcol = (0, 32, 128)
- lit_vcol = (0, 200, 255)
- lit_wcol = (0, 255, 255)
- vwarm_fn = lambda u: 3.6 * (u - 0.0)
- wwarm_fn = lambda u: 1.5 * (u - 0.3333)
- vcool_fn = lambda u: 1 - (7.0 * (u - 0.125))
- wcool_fn = lambda u: 1 - u
- default_fade_fn = lambda u: ((6 * u) // 1) & 1
- vfader = 0.0
- wfader = 0.0
- fade_v = False
- fade_w = False
- shimmer_col = (75, 185, 255)
- full_circle = 36 * 0x040000
- abs_angle = sg_state.ref_sector * 0x040000
- if sg_state.reversing:
- abs_angle -= sg_state.rel_angle
- if abs_angle < 0: abs_angle += full_circle
- else:
- abs_angle += sg_state.rel_angle
- if abs_angle >= full_circle: abs_angle -= full_circle
- # Ring
- ibr = 0.73 * max_r # Inner body radius
- icrr = 0.75 * max_r # Inner constellation ring radius
- ocrr = 0.86 * max_r # Outer constellation ring radius
- obr = 0.96 * max_r # Outer body radius
- pg.draw.circle(surface, body_col, C, ibr, 1)
- pg.draw.circle(surface, body_col, C, obr, 1)
- pg.draw.circle(surface, cr_col, C, icrr, 1)
- pg.draw.circle(surface, cr_col, C, ocrr, 1)
- # Ring metrics
- r0 = icrr - 0.07 * max_r # Inner extent of sector tick
- r1 = icrr - 0.04 * max_r # Outer extent of sector tick
- hsa = (2 * np.pi / 36) / 2 # Half sector angle in radians
- crr = icrr + 0.5 * (ocrr - icrr) # Constellation ring radius
- cr = 0.3 * (ocrr - icrr) # Constellation marker radius
- # Dialling state
- if sg_state.reversing:
- dialling_aix = sg_state.ref_sector - (sg_state.rel_angle >> 18)
- if dialling_aix < 0: dialling_aix += 36
- else:
- dialling_aix = sg_state.ref_sector + (sg_state.rel_angle >> 18)
- if dialling_aix >= 36: dialling_aix -= 36
- # Locked chevron positions
- if sg_state.incoming:
- lcps = [(1 + i) % 9 for i in range(sg_state.dial_seq_ix)]
- else:
- n = min(sg_state.dial_seq_ix, len(sg_param.lock_sequence))
- lcps = [chevron_pos(sg_param.lock_sequence[i])
- for i in range(n)]
- acp = -1 # Active chevron position (-1 means none)
- if sg_state.state in (
- SgState.Dialling,
- SgState.Incoming,
- SgState.FinalChevron
- ):
- if sg_state.dial_seq_ix >= 1:
- if sg_state.locking:
- if sg_state.incoming:
- acp = sg_state.dial_seq_ix
- if sg_state.state == SgState.FinalChevron:
- acp = 0
- else:
- x = sg_state.dial_seq_ix - 1
- if x < len(sg_param.lock_sequence):
- if sg_param.lock_sequence[x]:
- acp = chevron_pos(sg_param.lock_sequence[x])
- # Constellations
- for aix in range(36):
- a = aix * 2 * np.pi / 36
- # Division line
- R = np.array([np.sin(a - hsa), -np.cos(a - hsa)])
- pg.draw.line(surface, cr_col, C + icrr * R, C + ocrr * R, 1)
- R = np.array([np.sin(a), -np.cos(a)])
- if hud:
- # Inner tick
- pg.draw.line(surface, cr_col, C + r0 * R, C + r1 * R, 1)
- # Constellation
- col = (0, 0, 0)
- lw = 1
- if sg_param.mw_emulation:
- # The ring displays all constellations and rotates the desired
- # constellations to the indexing chevron at the top.
- if sg_state.state >= SgState.Idle:
- col = ring_colour_at(aix * 0x040000 - abs_angle, mw_ring_colours)
- lw = 2
- else:
- # The ring is blank except for the roving constellation and the
- # constellations already brought to their (locked) chevrons,
- if sg_state.state == SgState.Idle:
- col = cr_col
- elif sg_state.state in (
- SgState.Dialling,
- SgState.Misdialled,
- SgState.AlignForIncoming,
- SgState.FinalChevron
- ):
- if SgState.AlignForIncoming:
- col = cr_col
- lw = 2
- if dialling_aix == aix:
- col = lit_sector_col
- lw = 2
- if aix & 3 == 0 and aix // 4 in lcps:
- col = lit_sector_col
- lw = 2
- if sg_state.state == SgState.FinalChevron:
- if sg_state.incoming:
- col = lit_sector_col
- lw = 2
- elif sg_state.state == SgState.Incoming:
- x = abs_angle >> 18
- if 1 <= aix <= x or sg_state.dial_seq_ix >= 9:
- col = lit_sector_col
- lw = 2
- else:
- col = cr_col
- elif sg_state.state == SgState.Opening:
- if sg_state.incoming:
- col = lit_sector_col
- lw = 2
- else:
- if aix & 3 == 0 and aix // 4 in lcps:
- col = lit_sector_col
- lw = 2
- else:
- if sg_param.opening_time > 0:
- t = sg_state.progress / (sg_param.opening_time << 4)
- else:
- t = 1.0
- t1 = max(0, min(1, 1.5 * (t - 0.333)))
- col = t1 * np.array(lit_sector_col)
- lw = 2
- elif sg_state.state == SgState.Open:
- col = lit_sector_col
- lw = 2
- elif sg_state.state == SgState.Closing:
- col = cr_col
- lw = 1
- elif sg_state.state == SgState.Dimming:
- if sg_state.accepted:
- col = cr_col
- lw = 1
- if sum(col) > 0:
- M = np.array([[R[0], R[1]], [-R[1], R[0]]])
- pg.draw.circle(surface, col, C + [crr, 0] @ M, cr, lw)
- # Chevrons
- wx0 = icrr + 1.00 * (ocrr - icrr)
- wx1 = obr * 1.025
- wy0 = 0.010 * obr
- wy1 = 0.065 * obr
- Wedge = np.array([
- [wx1, wy1],
- [wx0, wy0],
- [wx0, -wy0],
- [wx1, -wy1],
- ])
- vx0 = wx0 - 0.010 * obr
- vx1 = wx1 - 0.021 * obr
- vx2 = vx1
- vx3 = vx0 - 0.020 * obr
- vy0 = 0.018 * obr
- vy1 = 0.068 * obr
- vy2 = 0.12 * obr
- vy3 = 0.020 * obr
- vx4 = vx0 + 0.2 * (vx3 - vx0)
- sx0 = vx1
- sx1 = sx0
- sx2 = sx0 + 0.005 * obr
- sx3 = sx0 + 0.80 * (wx1 - sx0)
- sx4 = wx1
- sy0 = wy1 - (wx1 - vx1)*(wy1 - wy0)/(wx1 - wx0)
- sy1 = sy0 + 0.17 * obr
- sy2 = sy1
- sy3 = sy0 + 0.5 * (sy1 - sy0)
- sy4 = wy1
- Frame = np.array([
- [vx1, vy1],
- [vx0, vy0],
- [vx0, -vy0],
- [vx1, -vy1],
- [vx2, -vy2],
- [vx3, -vy3],
- [vx3, vy3],
- [vx2, vy2],
- ])
- Indexer = np.array([
- [vx1, vy1],
- [vx0, vy0],
- [vx0, -vy0],
- [vx1, -vy1],
- [vx2, -vy2],
- [vx3, -vy3],
- [vx4, -vy3],
- [vx4, vy3],
- [vx3, vy3],
- [vx2, vy2],
- ])
- Shoulder = np.array([
- [sx0, sy0],
- [sx1, sy1],
- [sx2, sy2],
- [sx3, sy3],
- [sx4, sy4],
- ])
- for i in range(9):
- shimmer_phase = 0.0
- do_shimmer = False
- a = np.pi * (-0.5 + (i - 0) * 2 / 9)
- R = np.array([np.cos(a), np.sin(a)])
- M = np.array([[R[0], R[1]], [-R[1], R[0]]])
- wcol = vcol = ch_col
- vlw = wlw = 1
- activating = False
- D = np.array([0, 0])
- F = Frame
- vfade_fn = default_fade_fn
- wfade_fn = default_fade_fn
- vfader = 0.0
- wfader = 0.0
- fade_v = False
- fade_w = False
- if sg_param.base_style == SgStyle.MilkyWay:
- S = Shoulder
- pg.draw.lines(surface, ch_col, False, C + D + S @ M, width=1)
- S = Shoulder @ np.array([[1, 0], [0, -1]])
- pg.draw.lines(surface, ch_col, False, C + D + S @ M, width=1)
- if sg_state.state in (
- SgState.Dialling,
- SgState.Misdialled,
- SgState.FinalChevron,
- SgState.Incoming,
- SgState.Opening,
- SgState.Open,
- SgState.Closing
- ):
- if i in lcps:
- if i == acp:
- activating = True
- else:
- activating = sg_state.state == SgState.FinalChevron
- if (not activating and (i in lcps or sg_state.state in
- (SgState.Opening, SgState.Open, SgState.Closing))):
- vlw = wlw = 2
- vcol = lit_vcol
- wcol = lit_wcol
- if sg_state.state == SgState.Incoming:
- if activating:
- u = sg_state.chev_progress / (sg_param.chev_warm_time << 4)
- vfader = wfader = u
- vfade_fn = vwarm_fn
- wfade_fn = wwarm_fn
- fade_v = fade_w = True
- else:
- if activating or (i == 0):
- t = (sg_state.chev_progress
- - (sg_param.chev_warm_start_time << 4))
- if t >= 0:
- u = t / (sg_param.chev_warm_time << 4)
- vfader = wfader = u
- vfade_fn = vwarm_fn
- wfade_fn = wwarm_fn
- fade_v = fade_w = True
- if sg_state.state == SgState.Dimming:
- if sg_state.accepted or i in lcps:
- u = sg_state.chev_progress / (sg_param.chev_fade_time << 4)
- vfader = wfader = u
- vfade_fn = vcool_fn
- wfade_fn = wcool_fn
- fade_v = fade_w = True
- if i == 0:
- F = Indexer
- if (sg_state.locking and (sg_state.state == SgState.FinalChevron
- or sg_state.state != SgState.Incoming)):
- ct = sg_param.chev_locking_time << 4
- # Open and close the indexing chevron.
- if ct > 0:
- x = sg_param.chev_clack_start_time << 4
- s = 1 # Clack (out)
- if sg_state.chev_progress < x:
- x = sg_param.chev_click_start_time << 4
- s = 0 # Click (in)
- u = 7.14 * (sg_state.chev_progress - x) / ct
- u = max(0.0, min(1.0, u))
- if s: u = 1.0 - u
- D[1] = u * 0.015 * max_r
- # Illumination (dimming, here)
- if sg_state.state != SgState.FinalChevron:
- t = (sg_state.chev_progress
- - (sg_param.chev_fade_start_time << 4))
- if t >= 0:
- vlw = wlw = 1
- wcol = vcol = ch_col
- fade_v = fade_w = False
- if t < sg_param.chev_fade_time << 4:
- u = t / (sg_param.chev_fade_time << 4)
- vfader = wfader = u
- vfade_fn = vcool_fn
- wfade_fn = wcool_fn
- fade_v = fade_w = True
- elif sg_param.base_style == SgStyle.Pegasus:
- if sg_state.state in (
- SgState.Dialling,
- SgState.Misdialled,
- SgState.FinalChevron,
- SgState.Incoming,
- SgState.Opening,
- SgState.Open,
- SgState.Closing
- ):
- if sg_state.state in (
- SgState.Dialling,
- SgState.Misdialled,
- SgState.FinalChevron,
- SgState.Incoming
- ):
- do_shimmer = True
- u = 40 * ((sg_state.shimmer_phase & 65535) / 65536)
- shimmer_phase = u % 1.0
- if sg_state.incoming and shimmer_phase != 0.0:
- shimmer_phase = 1.0 - shimmer_phase
- if i in lcps:
- if i == acp:
- activating = True
- else:
- activating = sg_state.state == SgState.FinalChevron
- if (activating or i in lcps or sg_state.state in
- (SgState.Opening, SgState.Open, SgState.Closing)):
- vfader = wfader = 1
- vfade_fn = vwarm_fn
- wfade_fn = wwarm_fn
- fade_v = fade_w = True
- if activating:
- u = 0.0
- t0 = 0
- if sg_state.state != SgState.Incoming:
- t0 = sg_param.chev_warm_start_time << 4
- t = sg_state.chev_progress - t0
- if 0 <= t < (sg_param.chev_warm_time << 4):
- u = t / (sg_param.chev_warm_time << 4)
- vfader = wfader = u
- vfade_fn = vwarm_fn
- wfade_fn = wwarm_fn
- fade_v = fade_w = True
- if sg_state.state == SgState.Dimming:
- if sg_state.accepted or i in lcps:
- u = sg_state.chev_progress / (sg_param.chev_fade_time << 4)
- vfader = wfader = u
- vfade_fn = vcool_fn
- wfade_fn = wcool_fn
- fade_v = fade_w = True
- else:
- # Uknown style
- F = Frame
- vcol = [128, 0, 255]
- wcol = [255, 0, 255]
- if do_shimmer:
- u = np.sin(np.sqrt(shimmer_phase * np.pi * np.pi)) ** 2
- vcol = vcol + u * (np.array(shimmer_col) - vcol)
- if fade_v:
- u = max(0.0, min(1.0, vfade_fn(vfader)))
- vcol = dim_vcol + u * (np.array(lit_vcol) - dim_vcol)
- vlw = 2
- if fade_w:
- u = max(0.0, min(1.0, wfade_fn(wfader)))
- wcol = dim_wcol + u * (np.array(lit_wcol) - dim_wcol)
- wlw = 2
- pg.draw.lines(surface, vcol, True, C + D + F @ M, width=vlw)
- pg.draw.lines(surface, wcol, True, C - D + Wedge @ M, width=wlw)
- if hud:
- # Angle
- a = abs_angle * 2 * np.pi / (36 * 0x40000)
- R = np.array([np.sin(a), -np.cos(a)])
- r0 = 0.55 * max_r
- r1 = 0.71 * max_r
- pg.draw.line(surface, (0, 255, 0), C + r0 * R, C + r1 * R, 1)
- if hud:
- # Dialled sector
- if sg_param.mw_emulation:
- if sg_state.state in (
- SgState.Dialling,
- SgState.Misdialled,
- SgState.FinalChevron
- ):
- ix = sg_state.dial_seq_ix
- scol = (255, 144, 0)
- if not sg_state.sweeping:
- if sg_state.state != SgState.Misdialled:
- scol = (128, 255, 0)
- ix -= 1
- else:
- scol = (128, 0, 192)
- if sg_state.state == SgState.Dialling:
- if sg_state.progress < 0.25 * (sg_param.dwell_time << 4):
- ix = -1
- if 0 <= ix < len(sg_param.dial_sequence):
- x = sg_param.dial_sequence[ix] & 63
- if x >= 36: x -= 36
- u = min(1, sg_state.progress / 125.0)
- r0 = ibr + 0.5 * (icrr - ibr)
- r0 = r0 * (0.4 + 0.6 * u)
- rect = pg.Rect((C - [r0, r0]), (2 * r0, 2 * r0))
- a = (abs_angle + x * 0x040000) * 2 * np.pi / (36 * 0x040000)
- a0 = a1 = 0.5 * np.pi - a
- a0 -= hsa * u
- a1 += hsa * u
- lw = 5 if u < 1 else 3
- pg.draw.arc(surface, scol, rect, a0, a1, lw)
- if hud:
- # Rotational speed (and direction)
- a = (sg_state.speed / sg_param.max_speed) * (2 * np.pi / 9)
- if sg_state.reversing: a = -a
- R = np.array([np.sin(a), -np.cos(a)])
- r0 = 0.66 * max_r
- r1 = 0.73 * max_r
- pg.draw.line(surface, (192, 240, 0), C + r0 * R, C + r1 * R, 1)
- rect = pg.Rect((C - [r0, r0]), (2 * r0, 2 * r0))
- a0 = a1 = 0.5 * np.pi
- if a < 0:
- a1 -= a
- else:
- a0 -= a
- pg.draw.arc(surface, (192, 240, 0), rect, a0, a1, 1)
- # Wibbly wobbly swirly thing erroneously called an "event horizon".
- if sg_state.state in (SgState.Opening, SgState.Open, SgState.Closing):
- if sg_state.state == SgState.Opening:
- u = sg_state.progress / (sg_param.opening_time << 4)
- elif sg_state.state == SgState.Closing:
- u = 1.0 - (sg_state.progress / (sg_param.closing_time << 4))
- else:
- u = 1.0
- r = u * 0.98 * ibr
- col = (255 - int(255 * u), 255 - int(127 * u), 255)
- pg.draw.circle(surface, col, C, r, 4)
- def draw_stargate_astate(surface, sg_param, sg_state):
- """Draw auxiliary stargate data according to the audio state,
- sg_param is the StargateParam object used to hold the dialling
- sequence and the behaviour and timing parameters.
- sg_state is the StargateState object finite state machine, the
- one intended for audio output, which is usally animated ahead
- of the visual state object in order to correct for sound latency
- and propagation delay.
- """
- size = np.array([surface.get_width(), surface.get_height()])
- C = size // 2
- max_r = 0.98 * min(C[0], C[1])
- # Angle
- abs_angle = sg_state.ref_sector * 0x040000
- if sg_state.reversing:
- abs_angle -= sg_state.rel_angle
- else:
- abs_angle += sg_state.rel_angle
- a = abs_angle * 2 * np.pi / (36 * 0x40000)
- R = np.array([np.sin(a), -np.cos(a)])
- r0 = 0.57 * max_r
- r1 = 0.69 * max_r
- pg.draw.line(surface, (255, 0, 0), C + r0 * R, C + r1 * R, 1)
- # Rotational speed (and direction)
- a = (sg_state.speed / sg_param.max_speed) * (2 * np.pi / 9)
- if sg_state.reversing: a = -a
- R = np.array([np.sin(a), -np.cos(a)])
- r0 = 0.68 * max_r
- r1 = 0.71 * max_r
- pg.draw.line(surface, (224, 0, 96), C + r0 * R, C + r1 * R, 1)
- rect = pg.Rect((C - [r0, r0]), (2 * r0, 2 * r0))
- a0 = a1 = 0.5 * np.pi
- if a < 0:
- a1 -= a
- else:
- a0 -= a
- pg.draw.arc(surface, (224, 0, 96), rect, a0, a1, 1)
- def traces_from_sg_state(sg_state, sg_param):
- """Fetch output control signals from a stargate.
- These are the signals that would look nice on a seven-channel
- oscilloscope.
- """
- click = False # MWE: Indexer opens
- clack = False # MWE: Indexer closes
- if sg_state.locking:
- if sg_state.state == SgState.Incoming:
- click = True
- else:
- t = sg_state.chev_progress
- click = t >= sg_param.chev_click_start_time << 4
- if sg_param.mw_emulation:
- if (sg_state.state == SgState.Dialling
- or sg_state.state == SgState.FinalChevron):
- clack = t >= sg_param.chev_clack_start_time << 4
- result = [
- sg_state.speed / sg_param.max_speed,
- int(sg_state.sweeping),
- int(sg_state.lurching),
- int(click),
- int(clack),
- int(sg_state.state in (SgState.Opening, SgState.Open)),
- int(sg_state.state == SgState.Dimming),
- ]
- return result
- def draw_traces(surface, dpix, old_values, new_values, styles):
- """Draw a stack of traces on a scrolling oscilloscope display.
- dpix is the number of pixels to scroll and update.
- A 2-pixel margin exists at the right edge to accomodate 4-pixel
- strokes which might othwerwise be clipped.
- """
- size = np.array([surface.get_width(), surface.get_height()])
- if 1 <= dpix < size[0]:
- surface.blit(
- surface, (0, 0), pg.Rect((dpix, 0), (size[0] - dpix, size[1])))
- if dpix >= 1:
- surface.fill(0x000000,
- pg.Rect((size[0] - dpix, 0), (dpix, size[1])))
- dpix1 = min(dpix, size[0])
- n = len(new_values)
- pen_margin = 2
- field_h = size[1] // n
- stack_h = field_h * n
- ch_h = field_h * 3 // 4
- top_baseline = (size[1] - stack_h) // 2 + field_h - (field_h - ch_h) // 2
- baseline = top_baseline
- field_x1 = size[0] - pen_margin
- def_col = (255, 0, 0)
- for oldv, newv, style in zip(old_values, new_values, styles):
- lw = style.get('lw', 1)
- x0 = field_x1 - dpix1
- y0 = baseline - ch_h * oldv
- x1 = field_x1
- y1 = baseline - ch_h * newv
- col = style.get('col', def_col)
- pg.draw.line(surface, col, (x0, y0), (x1, y1), lw)
- baseline += field_h
- class MainCmd (IntEnum):
- """Command values to insulate the input system from business logic."""
- Null = 0
- Power = 1
- CutPower = 2
- Open = 3
- Incoming = 4
- Close = 5
- Style = 6
- HUD = 7
- def main():
- """Run the pretty stargate simulation."""
- print(help_msg)
- test_angle = 0.0
- pg.init()
- clock = pg.time.Clock()
- # Requested screen size
- rss = (960, 960)
- rss = (800, 800)
- window_style = 0 # FULLSCREEN
- best_depth = pg.display.mode_ok(rss, window_style, 32)
- screen = pg.display.set_mode(rss, window_style, best_depth)
- screen_size = screen.get_width(), screen.get_height()
- pg.display.set_caption("Stargate Animation Modelling.")
- # Oscilloscope canvases
- sw = screen_size[0] * 45 // 100
- sh = screen_size[1] * 45 // 100
- scope_rect = pg.Rect(
- ((screen_size[0] - sw) // 2, (screen_size[1] - sh) // 2),
- (sw, sh)
- )
- vscope_surface = pg.Surface(scope_rect.size, screen.get_bitsize(), screen)
- vscope_surface.fill(0x000000)
- ascope_surface = pg.Surface(scope_rect.size, screen.get_bitsize(), screen)
- ascope_surface.fill(0x000000)
- scope_time_err = 0
- anim_counter = 0
- max_fps = 100
- dampened_fps = max_fps
- delta_time = 1.0 / max_fps
- sg_param = StargateParam()
- sg_vstate = StargateState()
- sg_astate = copy.copy(sg_vstate)
- use_hud = True
- old_vvals = [0]
- old_avals = [0]
- delta_ms = clock.tick(max_fps)
- delta_ms = clock.tick(max_fps)
- visual_delay = 250
- visual_delay_countdown = 0
- visual_delay_error = 0
- print(f"Sound propagation\ndelay correction: {visual_delay}ms\n")
- mw_ring_colours = build_mw_ring_colours()
- do_exit = False
- while not do_exit:
- cmd = MainCmd.Null
- for event in pg.event.get():
- if event.type == pg.QUIT:
- do_exit = True
- print("[Quit]")
- elif event.type == pg.KEYUP and event.key == pg.K_ESCAPE:
- do_exit = True
- print("[ESC] Quit")
- elif event.type == pg.KEYUP:
- if event.key == pg.K_q:
- do_exit = True
- print("[Q] Quit")
- elif event.type == pg.KEYDOWN:
- if event.key == pg.K_p:
- print("[P] Power")
- cmd = MainCmd.Power
- if event.key == pg.K_x:
- print("[X] Cut Power")
- cmd = MainCmd.CutPower
- if event.key == pg.K_o:
- print("[O] Dial Out")
- cmd = MainCmd.Open
- if event.key == pg.K_i:
- print("[I] Incoming")
- cmd = MainCmd.Incoming
- if event.key == pg.K_c:
- print("[C] Close")
- cmd = MainCmd.Close
- if event.key == pg.K_s:
- print("[S] Style")
- cmd = MainCmd.Style
- if event.key == pg.K_h:
- print("[H] HUD")
- cmd = MainCmd.HUD
- if cmd == MainCmd.Power:
- if (sg_vstate.state == SgState.Off
- and sg_astate.state == SgState.Off):
- sg_vstate.state = SgState.Idle
- sg_astate = copy.copy(sg_vstate)
- if cmd == MainCmd.CutPower:
- sg_vstate.state = SgState.Off
- sg_astate = copy.copy(sg_vstate)
- if cmd == MainCmd.Style:
- if (sg_vstate.state <= SgState.Idle
- and sg_astate.state <= SgState.Idle):
- sg_param.set_style((sg_param.style + 1) % len(SgStyle))
- print(f"Style: {sg_param.style}")
- if cmd == MainCmd.Open:
- if (sg_vstate.state == SgState.Idle
- and sg_astate.state == SgState.Idle):
- if not sg_astate.open_req and not sg_vstate.open_req:
- sg_vstate.incoming = False
- sg_vstate.aborted = False
- sg_astate = copy.copy(sg_vstate)
- sg_astate.open_req = True
- visual_delay_countdown = visual_delay
- if cmd == MainCmd.Incoming:
- if (sg_vstate.state == SgState.Idle
- and sg_astate.state == SgState.Idle):
- if not sg_astate.open_req and not sg_vstate.open_req:
- sg_vstate.incoming = True
- sg_vstate.aborted = False
- sg_astate = copy.copy(sg_vstate)
- sg_astate.open_req = True
- visual_delay_countdown = visual_delay
- if cmd == MainCmd.Close:
- if sg_vstate.open_req and sg_astate.open_req:
- sg_astate.open_req = False
- visual_delay_countdown = visual_delay
- if cmd == MainCmd.HUD:
- use_hud = not use_hud
- sg_vstate.logging = True
- sg_astate.logging = False
- # Render
- screen.fill((0, 0, 0))
- screen_size = np.array([screen.get_width(), screen.get_height()])
- C = screen_size // 2
- max_r = 0.98 * min(C[0], C[1])
- mwrcs = mw_ring_colours
- draw_stargate_vstate(screen, sg_param, sg_vstate, mwrcs, hud=use_hud)
- if use_hud:
- draw_stargate_astate(screen, sg_param, sg_astate)
- # Floor
- R = np.array([1, 0]) * max_r
- x = max_r
- y = 0.63 * max_r
- pg.draw.line(screen, (128, 48, 0), C + [-x, y], C + [x, y], 3)
- # Oscilloscope
- scope_time = 5000
- a0 = dict(lw=4, col=pg.Color(96, 0, 255))
- v0 = dict(lw=1, col=pg.Color(0, 100, 160))
- a1 = dict(lw=4, col=pg.Color(255, 32, 0))
- v1 = dict(lw=1, col=pg.Color(0, 170, 0))
- a2 = dict(lw=4, col=pg.Color(200, 180, 0))
- v2 = dict(lw=1, col=pg.Color(0, 140, 240))
- a3 = dict(lw=4, col=pg.Color(200, 0, 160))
- v3 = dict(lw=1, col=pg.Color(0, 180, 140))
- astyles = [a0, a2, a2, a1, a1, a3, a1]
- vstyles = [v0, v2, v2, v1, v1, v3, v1]
- avals = traces_from_sg_state(sg_astate, sg_param)
- vvals = traces_from_sg_state(sg_vstate, sg_param)
- sw = scope_rect.width - 4
- sh = scope_rect.height
- dpix0 = delta_ms * sw / scope_time - scope_time_err
- dpix = int(round(dpix0))
- scope_time_err = dpix - dpix0
- dpix = min(sw, dpix)
- draw_traces(ascope_surface, dpix, old_avals, avals, astyles)
- draw_traces(vscope_surface, dpix, old_vvals, vvals, vstyles)
- old_vvals = vvals
- old_avals = avals
- delay_pix = int(sw * visual_delay / scope_time)
- y0, y1 = scope_rect.top, scope_rect.bottom
- x0 = scope_rect.left + sw
- x1 = x0 - delay_pix
- if use_hud:
- pg.draw.line(screen, 0x007700, (x0, y0), (x0, y1), 1)
- pg.draw.line(screen, 0x770000, (x1, y0), (x1, y1), 1)
- screen.blit(ascope_surface, scope_rect, special_flags=pg.BLEND_ADD)
- screen.blit(vscope_surface, scope_rect, special_flags=pg.BLEND_ADD)
- if use_hud:
- # Numeric displays
- w = screen.get_width()
- std_digit_width = int(round(w * 20.0 / 960.0))
- digit_width = std_digit_width
- digit_height = 2 * digit_width
- # Frames per second
- #cx = 0.01 * max_r
- #x = cx - 4 * 1.6 * digit_width
- x = 10
- fps_rect = pg.Rect((x, 10), (digit_width, digit_height))
- fps = 1.0 / delta_time
- weight = 0.1
- dampened_fps = dampened_fps + weight * (fps - dampened_fps)
- nstr = f"{dampened_fps:5.1f}"
- draw_nstr_7seg(screen, fps_rect, 0xFF00CC, nstr, seg_lw=3,
- small_decimals=True)
- # Style index
- sn = 1 + int(sg_param.style)
- nstr = f"{sn}"
- x = screen_size[0] - 10 - digit_width * (2 + len(nstr))
- sn_rect = pg.Rect((x, 10), (digit_width, digit_height))
- draw_nstr_7seg(screen, sn_rect, 0x0099CC, nstr, seg_lw=3),
- if False:
- # Test graphic to verify timing
- R = np.array([np.sin(test_angle), -np.cos(test_angle)]) * 0.71 * max_r
- pg.draw.line(screen, (255, 60, 0), C, C + R, 1)
- pg.display.update()
- # Animate and integrate
- delta_ms = clock.tick(max_fps)
- delta_time = delta_ms / 1000.0
- sg_vstate.integrate(sg_param, delta_ms)
- sg_astate.integrate(sg_param, delta_ms)
- if visual_delay_countdown > 0:
- if visual_delay_countdown >= delta_ms:
- visual_delay_countdown -= delta_ms
- visual_delay_error = 0
- else:
- visual_delay_error = delta_ms - visual_delay_countdown
- visual_delay_countdown = 0
- if visual_delay_countdown == 0:
- sg_vstate.open_req = sg_astate.open_req
- anim_counter += delta_ms
- test_angle += 2.0/60.0 * np.pi * delta_time
- if __name__ == '__main__':
- main()
- print("Done!")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement