├── Assets ├── .DS_Store ├── Others │ ├── Front.PNG │ └── Banner.PNG ├── FocusEntity │ ├── .DS_Store │ ├── Sources │ │ ├── .DS_Store │ │ └── FocusEntity │ │ │ └── .DS_Store │ └── LICENSE ├── Markup Images │ ├── KCL.PNG │ ├── KVL.PNG │ ├── Series.PNG │ ├── OhmsLaw.PNG │ ├── Parallel.PNG │ ├── Capacitance.PNG │ └── Inductance.PNG ├── USDZ Models │ ├── Battery.usdz │ ├── Ground.usdz │ ├── Source.usdz │ ├── ArrowSign.usdz │ ├── Capacitor.usdz │ ├── Inductor.usdz │ ├── MinusSign.usdz │ ├── PlusSign.usdz │ ├── Resistor.usdz │ ├── WireCross.usdz │ ├── WireCorner.usdz │ ├── WireStraight.usdz │ └── WireTshape.usdz └── Circuit Grid Images │ ├── Arrow.PNG │ ├── Diode.PNG │ ├── Battery.PNG │ ├── Ground1.PNG │ ├── Ground2.PNG │ ├── Ground3.PNG │ ├── Source.PNG │ ├── Inductor.PNG │ ├── LightBulb.PNG │ ├── Resistor.PNG │ ├── WireColor.PNG │ ├── WireCross.PNG │ ├── NegativeSign.PNG │ ├── PositiveSign.PNG │ ├── WireCornerDL.PNG │ ├── WireCornerDR.PNG │ ├── WireCornerUL.PNG │ ├── WireCornerUR.PNG │ ├── WireGround1.PNG │ ├── WireGround2.PNG │ ├── WireStraight.PNG │ ├── CapacitorHead.PNG │ ├── CapacitorShape.PNG │ ├── InductorShape.PNG │ ├── WireCornerShape.PNG │ ├── WireCrossShape.PNG │ ├── WireGroundShape.PNG │ ├── WireTshapeDLR.PNG │ ├── WireTshapeDUL.PNG │ ├── WireTshapeDUR.PNG │ ├── WireTshapeShape.PNG │ ├── WireTshapeULR.PNG │ ├── WireStraightShape.PNG │ └── WireCondensedShape.PNG ├── Screenshots ├── Header.PNG ├── Screenshot-1.PNG ├── Screenshot-2.PNG ├── Screenshot-3.PNG ├── Screenshot-4.PNG └── Screenshot-5.PNG ├── CircuitPlay.playgroundbook └── Contents │ ├── UserModules │ ├── CircuitKit.playgroundmodule │ │ └── Sources │ │ │ ├── Location.swift │ │ │ ├── Circuit+Errors.swift │ │ │ ├── Circuit+ComponentActions.swift │ │ │ ├── ComponentProtocols.swift │ │ │ ├── ComponentEnums.swift │ │ │ ├── CircuitGrid.swift │ │ │ ├── Circuit.swift │ │ │ ├── Circuit+Debug.swift │ │ │ ├── UnitEnums.swift │ │ │ ├── Presets.swift │ │ │ ├── Circuit+Navigation.swift │ │ │ ├── LocationEnums.swift │ │ │ ├── Contacts.swift │ │ │ └── Units.swift │ ├── CircuitUI.playgroundmodule │ │ └── Sources │ │ │ ├── ImageView.swift │ │ │ ├── BlurView.swift │ │ │ ├── NameView.swift │ │ │ ├── AddButon.swift │ │ │ ├── ViewEnums.swift │ │ │ ├── GridLines.swift │ │ │ ├── ComponentView.swift │ │ │ ├── NextStepView.swift │ │ │ ├── CurrentSignView.swift │ │ │ ├── InterfacePicker.swift │ │ │ ├── InductorView.swift │ │ │ ├── ChevronButton.swift │ │ │ ├── CapacitorView.swift │ │ │ ├── SignsView.swift │ │ │ ├── MeasurementView.swift │ │ │ ├── StatePickerView.swift │ │ │ ├── SideButtonsView.swift │ │ │ ├── MainView.swift │ │ │ ├── ArrowsView.swift │ │ │ ├── WiredComponentView.swift │ │ │ ├── ARContainerView.swift │ │ │ ├── Environment.swift │ │ │ ├── DockView.swift │ │ │ ├── GroundView.swift │ │ │ ├── InfoView.swift │ │ │ ├── WireView.swift │ │ │ ├── GridView.swift │ │ │ ├── SlotView.swift │ │ │ └── MeasurementEditView.swift │ ├── CircuitNetwork.playgroundmodule │ │ └── Sources │ │ │ ├── Model.swift │ │ │ ├── Node.swift │ │ │ ├── Branch.swift │ │ │ ├── ResistorModel.swift │ │ │ ├── CurrentSourceModel.swift │ │ │ ├── InductorModel.swift │ │ │ ├── CapacitorModel.swift │ │ │ ├── VoltageSourceModel.swift │ │ │ ├── Network.swift │ │ │ └── Network+Solver.swift │ └── FocusEntity.playgroundmodule │ │ └── Sources │ │ ├── float4x4+Extension.swift │ │ ├── FocusEntity+Colored.swift │ │ ├── FocusEntityComponent.swift │ │ ├── FocusEntity+Classic.swift │ │ ├── FocusEntity+Segment.swift │ │ └── FocusEntity+Alignment.swift │ ├── PublicResources │ ├── Arrow.PNG │ ├── Diode.PNG │ ├── Front.PNG │ ├── KCL.PNG │ ├── KVL.PNG │ ├── Banner.PNG │ ├── Battery.PNG │ ├── Ground.usdz │ ├── Ground1.PNG │ ├── Ground2.PNG │ ├── Ground3.PNG │ ├── OhmsLaw.PNG │ ├── Series.PNG │ ├── Source.PNG │ ├── Source.usdz │ ├── ArrowSign.usdz │ ├── Battery.usdz │ ├── Capacitor.usdz │ ├── Inductance.PNG │ ├── Inductor.PNG │ ├── Inductor.usdz │ ├── LightBulb.PNG │ ├── MinusSign.usdz │ ├── Parallel.PNG │ ├── PlusSign.usdz │ ├── Resistor.PNG │ ├── Resistor.usdz │ ├── WireColor.PNG │ ├── WireCross.PNG │ ├── WireCross.usdz │ ├── Capacitance.PNG │ ├── NegativeSign.PNG │ ├── PositiveSign.PNG │ ├── WireCorner.usdz │ ├── WireCornerDL.PNG │ ├── WireCornerDR.PNG │ ├── WireCornerUL.PNG │ ├── WireCornerUR.PNG │ ├── WireGround1.PNG │ ├── WireGround2.PNG │ ├── WireStraight.PNG │ ├── WireTshape.usdz │ ├── CapacitorHead.PNG │ ├── CapacitorShape.PNG │ ├── InductorShape.PNG │ ├── WireCornerShape.PNG │ ├── WireCrossShape.PNG │ ├── WireGroundShape.PNG │ ├── WireStraight.usdz │ ├── WireTshapeDLR.PNG │ ├── WireTshapeDUL.PNG │ ├── WireTshapeDUR.PNG │ ├── WireTshapeShape.PNG │ ├── WireTshapeULR.PNG │ ├── WireStraightShape.PNG │ └── WireCondensedShape.PNG │ ├── Chapters │ └── CircuitPlay.playgroundchapter │ │ ├── Pages │ │ ├── Sandbox.playgroundpage │ │ │ ├── Manifest.plist │ │ │ └── main.swift │ │ ├── Inductance.playgroundpage │ │ │ ├── Manifest.plist │ │ │ └── main.swift │ │ ├── Capacitance.playgroundpage │ │ │ ├── Manifest.plist │ │ │ └── main.swift │ │ ├── Introduction.playgroundpage │ │ │ ├── Manifest.plist │ │ │ └── main.swift │ │ ├── Ohm's Law.playgroundpage │ │ │ ├── Manifest.plist │ │ │ └── main.swift │ │ ├── Series Circuits.playgroundpage │ │ │ ├── Manifest.plist │ │ │ └── main.swift │ │ └── Parallel Circuits.playgroundpage │ │ │ ├── Manifest.plist │ │ │ └── main.swift │ │ └── Manifest.plist │ └── Manifest.plist └── README.md /Assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/.DS_Store -------------------------------------------------------------------------------- /Assets/Others/Front.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Others/Front.PNG -------------------------------------------------------------------------------- /Screenshots/Header.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Screenshots/Header.PNG -------------------------------------------------------------------------------- /Assets/Others/Banner.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Others/Banner.PNG -------------------------------------------------------------------------------- /Assets/FocusEntity/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/FocusEntity/.DS_Store -------------------------------------------------------------------------------- /Assets/Markup Images/KCL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Markup Images/KCL.PNG -------------------------------------------------------------------------------- /Assets/Markup Images/KVL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Markup Images/KVL.PNG -------------------------------------------------------------------------------- /Screenshots/Screenshot-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Screenshots/Screenshot-1.PNG -------------------------------------------------------------------------------- /Screenshots/Screenshot-2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Screenshots/Screenshot-2.PNG -------------------------------------------------------------------------------- /Screenshots/Screenshot-3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Screenshots/Screenshot-3.PNG -------------------------------------------------------------------------------- /Screenshots/Screenshot-4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Screenshots/Screenshot-4.PNG -------------------------------------------------------------------------------- /Screenshots/Screenshot-5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Screenshots/Screenshot-5.PNG -------------------------------------------------------------------------------- /Assets/Markup Images/Series.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Markup Images/Series.PNG -------------------------------------------------------------------------------- /Assets/USDZ Models/Battery.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/Battery.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/Ground.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/Ground.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/Source.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/Source.usdz -------------------------------------------------------------------------------- /Assets/Markup Images/OhmsLaw.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Markup Images/OhmsLaw.PNG -------------------------------------------------------------------------------- /Assets/Markup Images/Parallel.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Markup Images/Parallel.PNG -------------------------------------------------------------------------------- /Assets/USDZ Models/ArrowSign.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/ArrowSign.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/Capacitor.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/Capacitor.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/Inductor.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/Inductor.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/MinusSign.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/MinusSign.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/PlusSign.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/PlusSign.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/Resistor.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/Resistor.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/WireCross.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/WireCross.usdz -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Arrow.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Arrow.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Diode.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Diode.PNG -------------------------------------------------------------------------------- /Assets/FocusEntity/Sources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/FocusEntity/Sources/.DS_Store -------------------------------------------------------------------------------- /Assets/Markup Images/Capacitance.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Markup Images/Capacitance.PNG -------------------------------------------------------------------------------- /Assets/Markup Images/Inductance.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Markup Images/Inductance.PNG -------------------------------------------------------------------------------- /Assets/USDZ Models/WireCorner.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/WireCorner.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/WireStraight.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/WireStraight.usdz -------------------------------------------------------------------------------- /Assets/USDZ Models/WireTshape.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/USDZ Models/WireTshape.usdz -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Battery.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Battery.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Ground1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Ground1.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Ground2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Ground2.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Ground3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Ground3.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Source.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Source.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Inductor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Inductor.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/LightBulb.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/LightBulb.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/Resistor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/Resistor.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireColor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireColor.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireCross.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireCross.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/NegativeSign.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/NegativeSign.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/PositiveSign.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/PositiveSign.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireCornerDL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireCornerDL.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireCornerDR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireCornerDR.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireCornerUL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireCornerUL.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireCornerUR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireCornerUR.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireGround1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireGround1.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireGround2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireGround2.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireStraight.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireStraight.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/CapacitorHead.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/CapacitorHead.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/CapacitorShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/CapacitorShape.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/InductorShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/InductorShape.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireCornerShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireCornerShape.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireCrossShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireCrossShape.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireGroundShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireGroundShape.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireTshapeDLR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireTshapeDLR.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireTshapeDUL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireTshapeDUL.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireTshapeDUR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireTshapeDUR.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireTshapeShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireTshapeShape.PNG -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireTshapeULR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireTshapeULR.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Location.swift: -------------------------------------------------------------------------------- 1 | public typealias Location = (x: Int, y: Int) 2 | 3 | -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireStraightShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireStraightShape.PNG -------------------------------------------------------------------------------- /Assets/FocusEntity/Sources/FocusEntity/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/FocusEntity/Sources/FocusEntity/.DS_Store -------------------------------------------------------------------------------- /Assets/Circuit Grid Images/WireCondensedShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/Assets/Circuit Grid Images/WireCondensedShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Arrow.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Arrow.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Diode.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Diode.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Front.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Front.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/KCL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/KCL.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/KVL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/KVL.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Banner.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Banner.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Battery.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Battery.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Ground.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Ground.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Ground1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Ground1.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Ground2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Ground2.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Ground3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Ground3.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/OhmsLaw.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/OhmsLaw.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Series.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Series.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Source.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Source.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Source.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Source.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/ArrowSign.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/ArrowSign.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Battery.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Battery.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Capacitor.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Capacitor.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Inductance.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Inductance.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Inductor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Inductor.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Inductor.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Inductor.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/LightBulb.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/LightBulb.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/MinusSign.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/MinusSign.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Parallel.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Parallel.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/PlusSign.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/PlusSign.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Resistor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Resistor.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Resistor.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Resistor.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireColor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireColor.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCross.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCross.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCross.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCross.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/Capacitance.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/Capacitance.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/NegativeSign.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/NegativeSign.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/PositiveSign.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/PositiveSign.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCorner.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCorner.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerDL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerDL.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerDR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerDR.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerUL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerUL.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerUR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerUR.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireGround1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireGround1.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireGround2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireGround2.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireStraight.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireStraight.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireTshape.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireTshape.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/CapacitorHead.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/CapacitorHead.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/CapacitorShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/CapacitorShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/InductorShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/InductorShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCornerShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCrossShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCrossShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireGroundShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireGroundShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireStraight.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireStraight.usdz -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeDLR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeDLR.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeDUL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeDUL.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeDUR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeDUR.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeULR.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireTshapeULR.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireStraightShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireStraightShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/PublicResources/WireCondensedShape.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjoseadolfo/CircuitPlay/HEAD/CircuitPlay.playgroundbook/Contents/PublicResources/WireCondensedShape.PNG -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Circuit+Errors.swift: -------------------------------------------------------------------------------- 1 | public enum CircuitError: Error { 2 | case LocationOutOfBoundsError 3 | case MeasurementNotFound 4 | } 5 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/ImageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct SquareImageView: View { 4 | private var imageName: String 5 | 6 | public init(_ imageName: String) { 7 | self.imageName = imageName 8 | } 9 | 10 | public var body: some View { 11 | Image(uiImage: UIImage(named: imageName + ".PNG")!) 12 | .resizable() 13 | .aspectRatio(1, contentMode: .fit) 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/BlurView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct BlurView: UIViewRepresentable { 4 | public init(){} 5 | 6 | public func makeUIView(context: Context) -> UIView { 7 | let view = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) 8 | DispatchQueue.main.async { 9 | view.superview?.superview?.backgroundColor = .clear 10 | } 11 | return view 12 | } 13 | 14 | public func updateUIView(_ uiView: UIView, context: Context) {} 15 | } 16 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Sandbox.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | Sandbox 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | PosterReference 12 | Front.PNG 13 | PlaygroundLoggingMode 14 | Off 15 | 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Inductance.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | Inductance 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | PosterReference 12 | Front.PNG 13 | PlaygroundLoggingMode 14 | Off 15 | 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Capacitance.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | Capacitance 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | PosterReference 12 | Front.PNG 13 | PlaygroundLoggingMode 14 | Off 15 | 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Introduction.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | Introduction 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | PlaygroundLoggingMode 12 | Off 13 | PosterReference 14 | Front.PNG 15 | 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Ohm's Law.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | Ohm's Law 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | PosterReference 12 | Front.PNG 13 | PlaygroundLoggingMode 14 | Off 15 | 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Series Circuits.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | Series Circuits 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | PosterReference 12 | Front.PNG 13 | PlaygroundLoggingMode 14 | Off 15 | 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Parallel Circuits.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | Parallel Circuits 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | PosterReference 12 | Front.PNG 13 | PlaygroundLoggingMode 14 | Off 15 | 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/Model.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | 3 | public protocol Model: CustomStringConvertible { 4 | var number: Int { get } 5 | var component: Connectable { get } 6 | var location: Location { get } 7 | 8 | var connectedNodes: [Direction : Int] { get set} 9 | 10 | var voltage: Double { get set } 11 | var current: Double { get set } 12 | 13 | var currentGoing: Direction? { get set } 14 | 15 | var power: Double { get } 16 | } 17 | 18 | public protocol TerminalDependentModel: Model { 19 | var positiveTerminalNode: Int? { get set } 20 | var negativeTerminalNode: Int? { get set } 21 | } 22 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | CircuitPlay 7 | Pages 8 | 9 | Introduction.playgroundpage 10 | Ohm's Law.playgroundpage 11 | Series Circuits.playgroundpage 12 | Parallel Circuits.playgroundpage 13 | Capacitance.playgroundpage 14 | Inductance.playgroundpage 15 | Sandbox.playgroundpage 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Circuit+ComponentActions.swift: -------------------------------------------------------------------------------- 1 | extension Circuit { 2 | public mutating func removeComponent(from location: Location) { 3 | circuitGrid[location] = Slot(component: BlankSpace()) 4 | } 5 | 6 | public mutating func moveComponent(from origin: Location, to destination: Location) { 7 | let selectedComp = circuitGrid[origin].component 8 | removeComponent(from: origin) 9 | replaceComponent(with: selectedComp, at: destination) 10 | } 11 | 12 | public mutating func replaceComponent(with comp: CircuitComponent, at destination: Location) { 13 | circuitGrid[destination] = Slot(component: comp) 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/Node.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | import Foundation 3 | 4 | public struct Node: CustomStringConvertible, Identifiable { 5 | public var number: Int 6 | public var description: String { "N" + String(number) } 7 | 8 | public var connectedNodes: [String : Int] = [:] 9 | public var connectedBranches: [Int] = [] 10 | public var connectedModels: [String] = [] 11 | 12 | public var openings: [(Location, Direction)] = [] 13 | public var locations: [Location] = [] 14 | 15 | public var voltage: Double = 0 16 | 17 | public var id = UUID() 18 | 19 | public init(_ number: Int) { 20 | self.number = number 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/Branch.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | 3 | public enum End { 4 | case first, last 5 | } 6 | 7 | public struct Branch: CustomStringConvertible { 8 | public var number: Int 9 | public var description: String { "B" + String(number) } 10 | public var path: [Location] 11 | public var connectedBranches: [Int: End] = [:] 12 | 13 | public var dependence: String? 14 | public var dependenceFirst: Location? 15 | public var dependenceLast: Location? 16 | 17 | public var isReversed: Bool = false 18 | public var current: Double = 0 19 | public init(_ count: Int, path: [Location]) { 20 | self.number = count 21 | self.path = path 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/NameView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct NameView: View { 5 | private var name: String 6 | public init(name: String) { 7 | self.name = name 8 | } 9 | 10 | public var body: some View { 11 | Text(name) 12 | .font(.system(size: 13.5, weight: .regular, design: .monospaced)) 13 | .foregroundColor(Color.primary) 14 | .frame(height: 20, alignment: .center) 15 | .padding(.horizontal, 4) 16 | .background(Color.gray.opacity(0.25)) 17 | .background(BlurView()) 18 | .background(Color.white.opacity(0)) 19 | .cornerRadius(4) 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/ResistorModel.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | 3 | public struct ResistorModel: Model { 4 | public var number: Int 5 | public var description: String { "R" + String(number + 1)} 6 | public var component: Connectable 7 | public var location: Location 8 | 9 | public var connectedNodes: [Direction : Int] = [:] 10 | 11 | public var resistance: Double = 0 12 | public var voltage: Double = 0 13 | public var current: Double = 0 14 | public var power: Double { voltage * current } 15 | 16 | public var currentGoing: Direction? 17 | 18 | public init(for resistor: Resistor, count: Int, location: Location) { 19 | component = resistor 20 | number = count 21 | self.location = location 22 | resistance = resistor.measurements[0].value 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/AddButon.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct AddButton: View { 5 | @EnvironmentObject var envi: CircuitEnvironment 6 | private var direction: Direction 7 | 8 | public init(to direction: Direction) { 9 | self.direction = direction 10 | } 11 | 12 | public var body: some View { 13 | Button { 14 | envi.selectedSlot = nil 15 | envi.circuit.shiftGrid(going: direction.opposite) 16 | } label: { 17 | Label("Add", 18 | systemImage: "plus") 19 | .labelStyle(IconOnlyLabelStyle()) 20 | .imageScale(.large) 21 | .font(Font.body.weight(.bold)) 22 | } 23 | .foregroundColor(Color.secondary) 24 | .buttonStyle(ChevronButtonStyle()) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/ViewEnums.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public enum ViewState: CaseIterable, Identifiable { 4 | case design 5 | case setup 6 | case play 7 | 8 | public var id: Int { 9 | self.hashValue 10 | } 11 | } 12 | 13 | public enum DragState { 14 | case unknown 15 | case valid 16 | } 17 | 18 | public enum ViewColor { 19 | case slotBackground 20 | 21 | var color: Color { 22 | switch self { 23 | case .slotBackground: 24 | return Color(UIColor.secondaryLabel) 25 | } 26 | } 27 | } 28 | 29 | extension Color { 30 | public func getBackground(for colorScheme: ColorScheme) -> Color { 31 | switch colorScheme { 32 | case .light: 33 | return Color(white: 0.5) 34 | case .dark: 35 | return Color(white: 0.65) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/FocusEntity.playgroundmodule/Sources/float4x4+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // float4x4+Extension.swift 3 | // FocusEntity 4 | // 5 | // Created by Max Cobb on 8/26/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | import simd 10 | 11 | internal extension float4x4 { 12 | /** 13 | Treats matrix as a (right-hand column-major convention) transform matrix 14 | and factors out the translation component of the transform. 15 | */ 16 | var translation: SIMD3 { 17 | get { 18 | let translation = columns.3 19 | return SIMD3(translation.x, translation.y, translation.z) 20 | } 21 | set(newValue) { 22 | columns.3 = SIMD4(newValue.x, newValue.y, newValue.z, columns.3.w) 23 | } 24 | } 25 | 26 | /** 27 | Factors out the orientation component of the transform. 28 | */ 29 | var orientation: simd_quatf { 30 | return simd_quaternion(self) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/ComponentProtocols.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol CircuitComponent: CustomStringConvertible { 4 | var componentType: ComponentType { get } 5 | var connectionType: ConnectionType { get } 6 | var measurements: [CircuitUnit] { get set} 7 | mutating func rotate(_ rotation: Rotation) 8 | } 9 | 10 | public protocol Connectable: CircuitComponent { 11 | var contacts: Contacts { get } 12 | var rotationCount: Double { get } 13 | } 14 | 15 | public protocol Conductor: Connectable {} 16 | 17 | public protocol TerminalDependent { 18 | var terminals: (positive: Direction, negative: Direction) { get } 19 | } 20 | 21 | extension Connectable { 22 | func setMeasurement(of unit: CircuitUnit, to value: Double) throws { 23 | guard let index = (measurements.firstIndex{$0.prefixValue == unit.prefixValue}) else { 24 | throw CircuitError.MeasurementNotFound } 25 | measurements[index] 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chapters 6 | 7 | CircuitPlay.playgroundchapter 8 | 9 | ContentIdentifier 10 | com.apple.playgrounds.blank 11 | ContentVersion 12 | 1.0 13 | DeploymentTarget 14 | ios-current 15 | DevelopmentRegion 16 | en 17 | EnableConsole 18 | 19 | SwiftVersion 20 | 5.3 21 | Name 22 | CircuitPlay 23 | Version 24 | 8.0 25 | UserAutoImportedAuxiliaryModules 26 | 27 | UserModuleMode 28 | Full 29 | HasMeaningfulResetPoint 30 | 31 | ImageReference 32 | Banner.PNG 33 | 34 | 35 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/GridLines.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct GridLines: Shape { 4 | private var columns: Int 5 | private var rows: Int 6 | 7 | public init(columns: Int, rows: Int){ 8 | self.columns = columns 9 | self.rows = rows 10 | } 11 | 12 | public func path(in rect: CGRect) -> Path { 13 | var xRange: ClosedRange = 0...(columns * 2) 14 | var yRange: ClosedRange = 0...(rows * 2) 15 | let offset: CGFloat = 0.5 16 | return Path { path in 17 | xRange.forEach { x in 18 | path.move(to: CGPoint(x: CGFloat(x * 64), y: -offset)) 19 | path.addLine(to: CGPoint(x: CGFloat(x * 64), y: rect.height + offset)) 20 | } 21 | yRange.forEach { y in 22 | path.move(to: CGPoint(x: -offset, y: CGFloat(y * 64))) 23 | path.addLine(to: CGPoint(x: rect.width + offset, y: CGFloat(y * 64))) 24 | } 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Assets/FocusEntity/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Max Fraser Cobb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Sandbox.playgroundpage/main.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Sandbox 3 | In this page, you will be free to create your own circuit! 4 | 5 | Here are some rules to remember when making a circuit: 6 | - Make sure there are no loose ends. Each terminal of a component should be connected to another terminal of a different component. 7 | - Each loop in a circuit should at least have one resistor. Otherwise, you would be createing an invalid circuit with a short. 8 | - Don't forget to add the ground. The ground serves a reference nodes so the simulator can determine the voltages of each node. 9 | - When using a capacitor or an inductor, always include a resistor in the loop as well. Without a resistor, the current or voltage across a capacitor or inductor will be an impulse, causing the values to be discontinuous. 10 | 11 | Have fun! 12 | */ 13 | import SwiftUI 14 | import PlaygroundSupport 15 | 16 | let preset: CircuitPreset = .blank 17 | PlaygroundPage.current.wantsFullScreenLiveView = true 18 | PlaygroundPage.current.needsIndefiniteExecution = true 19 | PlaygroundPage.current.setLiveView( 20 | MainView() 21 | .environmentObject(CircuitEnvironment(circuit: preset.circuit)) 22 | .ignoresSafeArea() 23 | ) 24 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/CurrentSourceModel.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | 3 | public struct CurrentSourceModel: TerminalDependentModel { 4 | public var number: Int 5 | public var description: String { "Is" + String(number + 1)} 6 | public var component: Connectable 7 | public var location: Location 8 | 9 | public var positiveTerminalNode: Int? 10 | public var negativeTerminalNode: Int? 11 | 12 | public var connectedNodes: [Direction : Int] = [:] 13 | 14 | public var voltage: Double = 0 15 | public var current: Double 16 | public var power: Double { voltage * current } 17 | 18 | public var currentGoing: Direction? 19 | 20 | public init(for currentSource: CurrentSource, count: Int, location: Location) { 21 | component = currentSource 22 | number = count 23 | self.location = location 24 | current = currentSource.measurements[0].value 25 | if current == 0 { 26 | currentGoing = nil 27 | } else if current > 0 { 28 | currentGoing = currentSource.terminals.positive 29 | } else { 30 | currentGoing = currentSource.terminals.negative 31 | } 32 | } 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/InductorModel.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | 3 | public struct InductorModel: TerminalDependentModel { 4 | public var number: Int 5 | public var description: String { "L" + String(number + 1)} 6 | public var component: Connectable 7 | public var location: Location 8 | 9 | public var positiveTerminalNode: Int? 10 | public var negativeTerminalNode: Int? 11 | 12 | public var timeStep: Double 13 | 14 | public var connectedNodes: [Direction : Int] = [:] 15 | 16 | public var inductance: Double 17 | public var equivalentResistance: Double { inductance / Double(timeStep)} 18 | public var equivalentVoltage: Double { -current * inductance / timeStep } 19 | 20 | public var voltage: Double = 0 21 | public var current: Double = 0.0000001 22 | public var power: Double { voltage * current } 23 | 24 | public var currentGoing: Direction? 25 | 26 | public init(for inductor: Inductor, count: Int, timeStep: Double, location: Location) { 27 | component = inductor 28 | number = count 29 | inductance = inductor.measurements[0].value 30 | self.location = location 31 | self.timeStep = timeStep 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/CapacitorModel.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | 3 | public struct CapacitorModel: TerminalDependentModel { 4 | public var number: Int 5 | public var description: String { "C" + String(number + 1)} 6 | public var component: Connectable 7 | public var location: Location 8 | 9 | public var positiveTerminalNode: Int? 10 | public var negativeTerminalNode: Int? 11 | 12 | public var timeStep: Double 13 | 14 | public var connectedNodes: [Direction : Int] = [:] 15 | 16 | public var capacitance: Double 17 | public var equivalentResistance: Double { Double(timeStep) / capacitance } 18 | public var equivalentCurrent: Double { voltage * capacitance / timeStep} 19 | 20 | public var voltage: Double = 0.0000001 21 | public var current: Double = 0 22 | public var power: Double { voltage * current } 23 | 24 | public var currentGoing: Direction? 25 | 26 | public init(for capacitor: Capacitor, count: Int, timeStep: Double, location: Location) { 27 | component = capacitor 28 | number = count 29 | capacitance = capacitor.measurements[0].value 30 | self.location = location 31 | self.timeStep = timeStep 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/FocusEntity.playgroundmodule/Sources/FocusEntity+Colored.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusEntity+Colored.swift 3 | // FocusEntity 4 | // 5 | // Created by Max Cobb on 8/26/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | #if canImport(ARKit) 10 | import RealityKit 11 | 12 | /// An extension of FocusEntity holding the methods for the "colored" style. 13 | public extension FocusEntity { 14 | 15 | internal func coloredStateChanged() { 16 | guard let coloredStyle = self.focus.coloredStyle else { 17 | return 18 | } 19 | var endColor: MaterialColorParameter 20 | if self.state == .initializing { 21 | endColor = coloredStyle.nonTrackingColor 22 | } else { 23 | endColor = self.onPlane ? coloredStyle.onColor : coloredStyle.offColor 24 | } 25 | if self.fillPlane?.model?.materials.count == 0 { 26 | self.fillPlane?.model?.materials = [SimpleMaterial()] 27 | } 28 | // Necessary for transparency. 29 | var modelMaterial = UnlitMaterial(color: .clear) 30 | modelMaterial.baseColor = endColor 31 | // Necessary for transparency. 32 | modelMaterial.tintColor = Material.Color.white.withAlphaComponent(0.995) 33 | self.fillPlane?.model?.materials[0] = modelMaterial 34 | } 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/ComponentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct ComponentView: View { 5 | private var component: CircuitComponent 6 | 7 | public init(component: CircuitComponent) { 8 | self.component = component 9 | } 10 | 11 | public var body: some View { 12 | switch component.componentType { 13 | case .wire: 14 | WireView(wire: component as! Wire) 15 | case .battery: 16 | WiredComponentView(component: component as! Connectable) 17 | case .resistor: 18 | WiredComponentView(component: component as! Connectable) 19 | case .currentSource: 20 | WiredComponentView(component: component as! Connectable) 21 | case .ground: 22 | GroundView(ground: component as! Ground) 23 | case .capacitor: 24 | CapacitorView(capacitor: component as! Capacitor) 25 | case .inductor: 26 | InductorView(inductor: component as! Inductor) 27 | default: 28 | Rectangle() 29 | .fill(Color.white.opacity(0.0001)) 30 | .aspectRatio(1.0, contentMode: .fit) 31 | .frame(width: .infinity, height: .infinity) 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/NextStepView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct NextStepView: View { 4 | @EnvironmentObject var envi: CircuitEnvironment 5 | public init() {} 6 | public var body: some View { 7 | GeometryReader { geo in 8 | Button(action: { 9 | do { 10 | try envi.network?.solveNetwork() 11 | } catch { 12 | 13 | } 14 | }){ 15 | Label("Next Time Step", systemImage: "forward.fill") 16 | .foregroundColor(Color.primary) 17 | } 18 | .buttonStyle(StepButtonStyle()) 19 | .position(x: geo.frame(in: .global).midX, y: geo.frame(in: .global).maxY - 135) 20 | } 21 | } 22 | } 23 | 24 | private struct StepButtonStyle: ButtonStyle { 25 | init(){} 26 | func makeBody(configuration: Configuration) -> some View { 27 | configuration.label 28 | .scaleEffect(configuration.isPressed ? 1.05 : 1.0) 29 | .padding(8) 30 | .background(BlurView()) 31 | .background(configuration.isPressed ? Color.blue.opacity(0.5) : Color.gray.opacity(0.25)) 32 | .shadow(radius: configuration.isPressed ? 7.5 : 0) 33 | .cornerRadius(6) 34 | .animation(.linear, value: configuration.isPressed) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/CurrentSignView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct CurrentSignView: View { 5 | private var direction: Direction 6 | 7 | public init(direction: Direction) { 8 | self.direction = direction 9 | } 10 | 11 | public var body: some View { 12 | GeometryReader{ geometry in 13 | SquareImageView("Arrow") 14 | .rotation3DEffect(.degrees(direction == .up || direction == .down ? 180 : 0), axis: (x: 1, y: 0, z: 0)) 15 | .frame(width: geometry.size.width / 8, height: geometry.size.height / 8) 16 | .position(CGPoint(x: 7 * geometry.size.width / 8, y: 5 * geometry.size.height / 8) ) 17 | SquareImageView("Arrow") 18 | .rotation3DEffect(.degrees(direction == .up || direction == .down ? 180 : 0), axis: (x: 1, y: 0, z: 0)) 19 | .frame(width: geometry.size.width / 8, height: geometry.size.height / 8) 20 | .position(CGPoint(x: 1 * geometry.size.width / 8, y: 5 * geometry.size.height / 8)) 21 | }.rotation3DEffect(.degrees(direction == .left ? 180 : 0), axis: (x: 0, y: 1, z: 0)) 22 | .rotation3DEffect(.degrees(direction == .up || direction == .down ? -90 : 0), axis: (x: 0, y: 0, z: 1)) 23 | .rotation3DEffect(.degrees(direction == .down ? 180 : 0), axis: (x: 1, y: 0, z: 0)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/InterfacePicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct InterfacePicker: View { 4 | @EnvironmentObject var envi: CircuitEnvironment 5 | 6 | public var body: some View { 7 | GeometryReader { geo in 8 | ZStack{ 9 | RoundedRectangle(cornerRadius: 8) 10 | .fill(Color.blue.opacity(0.5)) 11 | .frame(width: 36, height: 36, alignment: .center) 12 | .opacity(envi.arMode ? 1 : 0) 13 | Toggle("", isOn: $envi.arMode) 14 | .toggleStyle(CheckMarkToggleStyle()) 15 | } 16 | .frame(width: 52, height: 52, alignment: .center) 17 | .background(BlurView()) 18 | .background(Color.gray.opacity(0.25)) 19 | .cornerRadius(12) 20 | .position(x: geo.frame(in: .global).midX, y: geo.frame(in: .global).minY + 192) 21 | }.animation(.spring(), value: envi.currentState) 22 | } 23 | } 24 | 25 | struct CheckMarkToggleStyle: ToggleStyle { 26 | func makeBody(configuration: Self.Configuration) -> some View { 27 | HStack { 28 | Button(action: { configuration.isOn.toggle() } ) 29 | { 30 | Image(systemName: "arkit") 31 | .foregroundColor(Color.primary) 32 | } 33 | } 34 | .font(.title) 35 | .padding(.horizontal) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Inductance.playgroundpage/main.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Inductance 3 | This page features a circuit with a resistor-inductor circuit. A capacitor is passive circuit component and it has the ability to store electric charge and oppose the current through it using the magnetic field generated by its coils. The voltage across a inductor is equal to the inductance of the inductor and the current change per unit time. 4 | 5 | ![Equation for Inductance](Inductance.PNG) 6 | 7 | **Task:** Induce My Current 8 | 1. Tap `Run My Code` button on the right. 9 | 2. Click on the `Solve` button. What is the voltage across the inductor? 10 | 3. Tap on the `Next Time Step` button. What happens to voltage and current? Does it increase or decrease? 11 | 4. Change the value of the inductance? How does it affect the rate of change of the voltage and current? 12 | 13 | In a direct current circuit, the inductor would have stored its full capacity after five time constants, thus, the inductor will cause the circuit act as an open circuit. 14 | 15 | Go to the [Next Page](@next) where it will be your time to shine. 16 | */ 17 | import SwiftUI 18 | import PlaygroundSupport 19 | 20 | let preset: CircuitPreset = .resistorInductor 21 | PlaygroundPage.current.wantsFullScreenLiveView = true 22 | PlaygroundPage.current.needsIndefiniteExecution = true 23 | PlaygroundPage.current.setLiveView( 24 | MainView() 25 | .environmentObject(CircuitEnvironment(circuit: preset.circuit)) 26 | .ignoresSafeArea() 27 | ) 28 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Capacitance.playgroundpage/main.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Capacitance 3 | This page features a circuit with a resistor-capacitor circuit. A capacitor is passive circuit component and it has the ability to store electric charge using an electric field. The current across a capacitor is equal to the capacitance of the capacitor and the voltage change per unit time. 4 | 5 | ![Equation for Capacitance](Capacitance.PNG) 6 | 7 | **Task:** Capacitate My Current 8 | 1. Tap `Run My Code` button on the right. 9 | 2. Click on the `Solve` button. What is the voltage across the capacitor? 10 | 3. Tap on the `Next Time Step` button. This should solve the circuit values after a time step of 10 millisecond. What happens to voltage and current? Does it increase or decrease? 11 | 4. Change the value of the capacitance? How does it affect the rate of change of the voltage and current? 12 | 13 | In a direct current circuit, the capacitor would have stored its full capacity after five time constants, thus, the capacitor will cause the circuit to short. 14 | 15 | Go to the [Next Page](@next) where you will learn about inductors. 16 | */ 17 | import SwiftUI 18 | import PlaygroundSupport 19 | 20 | let preset: CircuitPreset = .resistorCapacitor 21 | PlaygroundPage.current.wantsFullScreenLiveView = true 22 | PlaygroundPage.current.needsIndefiniteExecution = true 23 | PlaygroundPage.current.setLiveView( 24 | MainView() 25 | .environmentObject(CircuitEnvironment(circuit: preset.circuit)) 26 | .ignoresSafeArea() 27 | ) 28 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/InductorView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct InductorView: View { 5 | private var inductor: Inductor 6 | 7 | private var rotationCount: Double { inductor.rotationCount } 8 | 9 | private var isVertical: Bool { inductor.contacts.orientation == .vertical} 10 | private var isFlippedY: Bool { 11 | return [-3.0,-2.0,1.0,2.0].contains(rotationCount.remainder(dividingBy: 4)) 12 | } 13 | 14 | private var isFlippedX: Bool { 15 | return [-2.0,-1.0,2.0,3.0].contains(rotationCount.remainder(dividingBy: 4)) 16 | } 17 | 18 | public init(inductor: Inductor) { 19 | self.inductor = inductor 20 | } 21 | 22 | public var body: some View { 23 | WireShape { 24 | SquareImageView("InductorShape") 25 | .rotation3DEffect( Angle(degrees: rotationCount * 90), 26 | axis: (x: 0, y: 0, z: 1)) 27 | .scaleEffect(1.5) 28 | } overlay: { 29 | SquareImageView("Inductor") 30 | .rotation3DEffect( Angle(degrees: isFlippedY ? 180 : 0), axis: (x: 0, y: 1, z: 0)) 31 | .rotation3DEffect( Angle(degrees: isFlippedX ? 180 : 0), axis: (x: 1, y: 0, z: 0)) 32 | .rotation3DEffect( Angle(degrees: isVertical ? 180 : 0), axis: (x: 0, y: 1, z: 0)) 33 | .rotation3DEffect( Angle(degrees: rotationCount * 90), axis: (x: 0, y: 0, z: 1)) 34 | } 35 | 36 | } 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Introduction.playgroundpage/main.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # CircuitPlay 3 | WWDC21 Swift Student Challenge Submission by Jose Adolfo Talactac 4 | 5 | ## Overview 6 | Hello, welcome to CircuitPlay! CircuitPlay is an interactive sandbox experience that allows you to design, setup, and simulate circuit. Each page in this playground can be experienced for 30 seconds or less. However, since this is a sandbox environment, feel free to do what ever you wish! 7 | 8 | ## Credits 9 | - [Barlow font](https://github.com/jpt/barlow) designed by Jeremy Tribby found in the Playground thumbnail and the images in each PlaygroundPage is used under the [Open Font License](https://github.com/jpt/barlow/blob/main/OFL.txt). 10 | - San Francisco font found in the Playground’s interface is accessed as part of iPadOS’ system fonts and used fairly within the license set by Apple Inc. for the development of software for Apple platforms. 11 | - [Focus Entity](https://github.com/maxxfrazer/FocusEntity) Swift Package by Max Cobb is used in the augmented reality component of the Playground for placing the model entity in the view. The package is used under the [MIT license](https://github.com/maxxfrazer/FocusEntity/blob/main/LICENSE). 12 | 13 | - Important: 14 | This Playground has an augmented reality component tested on an iPad Pro 12.9-inch (3rd Generation) model. It is recommended to run this Playground in a full screen Live View, without any other app running in Split View and Slide Over and without any videos running in Picture-in-Picture. 15 | 16 | Go to the [Next Page](@next) once you're ready. 17 | */ 18 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/VoltageSourceModel.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | 3 | public struct VoltageSourceModel: TerminalDependentModel { 4 | public var number: Int 5 | public var description: String { "Vs" + String(number + 1)} 6 | public var component: Connectable 7 | public var location: Location 8 | 9 | public var positiveTerminalNode: Int? 10 | public var negativeTerminalNode: Int? 11 | 12 | public var connectedNodes: [Direction : Int] = [:] 13 | 14 | public var voltage: Double 15 | public var current: Double = 0 16 | public var power: Double { voltage * current } 17 | 18 | public var currentGoing: Direction? 19 | 20 | public var signal: SignalType = .direct 21 | public var amplitude: Double = 0 22 | public var frequency: Double = 0 23 | 24 | public init(for voltageSource: Connectable, count: Int, location: Location) { 25 | component = voltageSource 26 | number = count 27 | voltage = voltageSource.measurements[0].value 28 | self.location = location 29 | 30 | let terminalDependent = voltageSource as! TerminalDependent 31 | 32 | if voltage == 0 { 33 | currentGoing = nil 34 | } else if voltage > 0 { 35 | currentGoing = terminalDependent.terminals.positive 36 | } else { 37 | currentGoing = terminalDependent.terminals.negative 38 | } 39 | 40 | if let source = voltageSource as? VoltageSource { 41 | amplitude = source.measurements[0].value 42 | frequency = source.measurements[1].value 43 | signal = source.signal 44 | } 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Parallel Circuits.playgroundpage/main.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Parallel Circuits 3 | This page features a parallel circuit with a voltage source and two resistors. In a parallel circuit, the equivalent resistance of the circuit is equal to the inverse of the sum of the inverse of the resistance of each resistor in parallel. 4 | 5 | ![Equation for the Equivalent Resistance in Parallel Circuits](Parallel.PNG) 6 | 7 | **Task:** Parallel Parking 8 | 1. Tap `Run My Code` button on the right. 9 | 2. Tap on the `Solve` button. What are the voltages across each resistor? Do they have the same values? 10 | 3. Try changing the value of each resistor. What happens to the voltages and current? 11 | 4. Add another resistor to the circuits in parallel. Observe how it affects the current. 12 | 5. What happens when we swap the voltage source for a current source? 13 | 14 | The current across each resistor in parallel is different because according to Kirchoff’s Current Law states that the sum of the current going into a node is equal to the sum of the current going out a node. In the original example, the voltage source pushes `1.00A` of current while the resistor pulls `500.00mA` of current each. 15 | 16 | ![Kirchoff's Current Law Equation](KCL.PNG) 17 | 18 | Go to the [Next Page](@next) where you will learn about capacitors. 19 | */ 20 | import SwiftUI 21 | import PlaygroundSupport 22 | 23 | let preset: CircuitPreset = .parallel 24 | PlaygroundPage.current.wantsFullScreenLiveView = true 25 | PlaygroundPage.current.needsIndefiniteExecution = true 26 | PlaygroundPage.current.setLiveView( 27 | MainView() 28 | .environmentObject(CircuitEnvironment(circuit: preset.circuit)) 29 | .ignoresSafeArea() 30 | ) 31 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/ChevronButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct ChevronButton: View { 5 | @Binding private var show: Bool 6 | private var showDirection: Direction 7 | private var hideDirection: Direction 8 | 9 | public init(_ show: Binding, direction: Direction) { 10 | self._show = show 11 | self.showDirection = direction 12 | self.hideDirection = direction.opposite 13 | } 14 | 15 | public var body: some View { 16 | Button { 17 | show.toggle() 18 | } label: { 19 | Label(show ? "Hide Components" : "Show Components", 20 | systemImage: hideDirection.chevron) 21 | .labelStyle(IconOnlyLabelStyle()) 22 | .imageScale(.large) 23 | .font(Font.body.weight(.bold)) 24 | .rotationEffect(show ? .degrees(180) : .zero) 25 | } 26 | .foregroundColor(Color.secondary) 27 | .buttonStyle(ChevronButtonStyle()) 28 | } 29 | } 30 | 31 | struct ChevronButtonStyle: ButtonStyle { 32 | func makeBody(configuration: Configuration) -> some View { 33 | configuration.label 34 | .frame(width: 32, height: 32, alignment: .center) 35 | .aspectRatio(1.0, contentMode: .fit) 36 | .padding(4) 37 | .background(configuration.isPressed ? Color.blue.opacity(0.5) : Color.gray.opacity(0.25)) 38 | .background(BlurView()) 39 | .clipShape(Circle()) 40 | .scaleEffect(configuration.isPressed ? 1.5 : 1.0) 41 | .shadow(radius: configuration.isPressed ? 7.5 : 0) 42 | .animation(.linear, value: configuration.isPressed) 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Series Circuits.playgroundpage/main.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Series Circuits 3 | This page features a simple series circuit with a voltage source and two resistors. In a series circuit, the equivalent resistance of the circuit is equal to the sum of the resistance of each resistor in series. 4 | 5 | ![Equation for the Equivalent Resistance in Series Circuits](Series.PNG) 6 | 7 | **Task:** Choo! Choo! 8 | 1. Tap `Run My Code` button on the right. 9 | 2. Tap on the `Solve` button. What are the voltages across each resistor? Do they have the same values? 10 | 3. Try changing the value of each resistor. What happens to the voltages and current? 11 | 4. Add another resistor to the circuits in series. Observe how it affects the voltage. 12 | 5. Swap the voltage source for a current source. Does current differ across each resistor? 13 | 14 | 15 | The voltage across each resistor in series is different because according to Kirchoff’s Voltage Law, the sum of voltage rise is equal to the sum of voltage drop. In our original example, The voltage source causes a voltage rise of `5.00V` and each resistor each drops the voltage by `2.50V`. The voltage rise is equal to the sum of the two voltage drops. 16 | 17 | ![Kirchoff's Voltage Law Equation](KVL.PNG) 18 | 19 | Go to the [Next Page](@next) where you will learn about resistors in parallel. 20 | */ 21 | import SwiftUI 22 | import PlaygroundSupport 23 | 24 | let preset: CircuitPreset = .series 25 | PlaygroundPage.current.wantsFullScreenLiveView = true 26 | PlaygroundPage.current.needsIndefiniteExecution = true 27 | PlaygroundPage.current.setLiveView( 28 | MainView() 29 | .environmentObject(CircuitEnvironment(circuit: preset.circuit)) 30 | .ignoresSafeArea() 31 | ) 32 | 33 | 34 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/CapacitorView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct CapacitorView: View { 5 | private var capacitor: Capacitor 6 | 7 | private var rotationCount: Double { capacitor.rotationCount } 8 | 9 | private var isVertical: Bool { capacitor.contacts.orientation == .vertical} 10 | private var isFlippedY: Bool { 11 | return [-3.0,-2.0,1.0,2.0].contains(rotationCount.remainder(dividingBy: 4)) 12 | } 13 | 14 | private var isFlippedX: Bool { 15 | return [-2.0,-1.0,2.0,3.0].contains(rotationCount.remainder(dividingBy: 4)) 16 | } 17 | 18 | public init(capacitor: Capacitor) { 19 | self.capacitor = capacitor 20 | } 21 | 22 | public var body: some View { 23 | ZStack { 24 | WireShape { 25 | SquareImageView("CapacitorShape") 26 | .rotation3DEffect( Angle(degrees: rotationCount * 90), 27 | axis: (x: 0, y: 0, z: 1)) 28 | .scaleEffect(1.5) 29 | } overlay: { 30 | SquareImageView("WireStraight") 31 | .rotation3DEffect(isVertical ? 32 | Angle(degrees: 90) : 33 | Angle(degrees: 0), axis: (x: 0, y: 0, z: 1)) 34 | } 35 | SquareImageView("CapacitorHead") 36 | .rotation3DEffect( Angle(degrees: isFlippedY ? 180 : 0), axis: (x: 0, y: 1, z: 0)) 37 | .rotation3DEffect( Angle(degrees: isFlippedX ? 180 : 0), axis: (x: 1, y: 0, z: 0)) 38 | .rotation3DEffect( Angle(degrees: rotationCount * 90), axis: (x: 0, y: 0, z: 1)) 39 | } 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/ComponentEnums.swift: -------------------------------------------------------------------------------- 1 | public enum ComponentType { 2 | case wire 3 | case spacer 4 | case ground 5 | case battery, voltageSource 6 | case currentSource 7 | case resistor 8 | case inductor, capacitor 9 | case diode 10 | 11 | public var name: String { 12 | switch self { 13 | case .wire: 14 | return "Wire" 15 | case .resistor: 16 | return "Resistor" 17 | case .battery: 18 | return "Voltage \n Source" 19 | case .ground: 20 | return "Ground" 21 | case .diode: 22 | return "Diode" 23 | case .inductor: 24 | return "Inductor" 25 | case .capacitor: 26 | return "Capacitor" 27 | case .currentSource: 28 | return "Current \n Source" 29 | case .voltageSource: 30 | return "Voltage \n Source" 31 | default: 32 | return "Unnamed" 33 | } 34 | } 35 | 36 | public var imageName: String { 37 | switch self { 38 | case .battery: 39 | return "Battery" 40 | case .currentSource, .voltageSource: 41 | return "Source" 42 | case .resistor: 43 | return "Resistor" 44 | case .diode: 45 | return "Diode" 46 | case .inductor: 47 | return "Inductor" 48 | case .capacitor: 49 | return "Capacitor" 50 | default: 51 | return "" 52 | } 53 | } 54 | } 55 | 56 | public enum SignalType: String, CaseIterable { 57 | case direct 58 | case pulse 59 | case sine 60 | 61 | public var rawValue: String { 62 | self.rawValue.capitalized 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/SignsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct SignsView: View { 5 | private var positiveDirection: Direction 6 | private var negativeDirection: Direction { positiveDirection.opposite } 7 | 8 | public init(positiveDirection: Direction) { 9 | self.positiveDirection = positiveDirection 10 | } 11 | 12 | public var body: some View { 13 | GeometryReader{ geometry in 14 | SquareImageView("PositiveSign") 15 | .frame(width: geometry.size.width / 8, height: geometry.size.height / 8) 16 | .position(SignOffset(geometry: geometry, direction: positiveDirection).offset) 17 | SquareImageView("NegativeSign") 18 | .frame(width: geometry.size.width / 8, height: geometry.size.height / 8) 19 | .position(SignOffset(geometry: geometry, direction: negativeDirection).offset) 20 | } 21 | } 22 | } 23 | 24 | private struct SignOffset { 25 | private var geometry: GeometryProxy 26 | private var direction: Direction 27 | public var offset: CGPoint { 28 | switch direction { 29 | case .down: 30 | return CGPoint(x: 3 * geometry.size.height / 8, y: 7 * geometry.size.width / 8) 31 | case .up: 32 | return CGPoint(x: 3 * geometry.size.height / 8, y: geometry.size.width / 8) 33 | case .left: 34 | return CGPoint(x: 1 * geometry.size.width / 8, y: 3 * geometry.size.height / 8) 35 | case .right: 36 | return CGPoint(x: 7 * geometry.size.width / 8, y: 3 * geometry.size.height / 8) 37 | } 38 | } 39 | 40 | public init(geometry: GeometryProxy, direction: Direction) { 41 | self.geometry = geometry 42 | self.direction = direction 43 | } 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/CircuitGrid.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import CoreGraphics 4 | 5 | public struct Slot: CustomStringConvertible, Identifiable { 6 | public var component: CircuitComponent 7 | public let id = UUID() 8 | 9 | public var zIndex: Int = 0 10 | public var gridIndex: Location = (x: 0, y: 0) 11 | public var selected: Bool = false 12 | 13 | public var assocNode: Int? 14 | public var assocModel: String? 15 | public var assocBranch: Int? 16 | 17 | public var description: String { component.description } 18 | 19 | public init(component: CircuitComponent) { 20 | self.component = component 21 | } 22 | 23 | public mutating func changeComponent(to newComponent: CircuitComponent){ 24 | self.component = newComponent 25 | } 26 | } 27 | 28 | public struct Row: Identifiable { 29 | public let id = UUID() 30 | public var slots: [Slot] 31 | 32 | public init(_ slots: Slot...) { 33 | self.slots = slots 34 | } 35 | 36 | public init(_ slots: [Slot]) { 37 | self.slots = slots 38 | } 39 | 40 | public mutating func append(_ slot: Slot) { 41 | slots.append(slot) 42 | } 43 | } 44 | 45 | public struct Grid { 46 | public var rows: [Row] 47 | 48 | public init(_ rows: Row...) { 49 | self.rows = rows 50 | } 51 | 52 | public init(_ rows: [Row]) { 53 | self.rows = rows 54 | } 55 | 56 | public mutating func append(_ row: Row) { 57 | rows.append(row) 58 | } 59 | 60 | public subscript(index: Location) -> Slot { 61 | get { 62 | return rows[index.y].slots[index.x] 63 | } 64 | set { 65 | rows[index.y].slots[index.x] = newValue 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/MeasurementView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct MeasurementView: View { 5 | @EnvironmentObject var envi: CircuitEnvironment 6 | @Binding private var measurement: CircuitUnit 7 | @State private var selected: Bool = false 8 | 9 | public init(measurement: Binding) { 10 | self._measurement = measurement 11 | } 12 | 13 | @State var showPopover = false 14 | 15 | public var body: some View { 16 | Button { 17 | showPopover = true 18 | } 19 | label: { 20 | HStack(spacing: 4){ 21 | Text(measurement.description) 22 | .font(.system(size: 13.5, weight: .regular, design: .monospaced)) 23 | if envi.currentState == .setup { 24 | button 25 | } 26 | } 27 | } 28 | .foregroundColor(Color.primary) 29 | .frame(height: 20, alignment: .center) 30 | .padding(.horizontal, 4) 31 | .background(Color.gray.opacity(selected ? 0 : 0.25)) 32 | .background(BlurView()) 33 | .background(Color.white.opacity(selected ? 0.5 : 0)) 34 | .cornerRadius(4) 35 | .animation(.spring(), value: selected) 36 | .popover(isPresented: $showPopover , arrowEdge: .top) { MeasurementEditView(measurement: $measurement, show: $showPopover) } 37 | .allowsHitTesting(envi.currentState == .setup) 38 | } 39 | 40 | var button: some View { 41 | Label("EDIT", systemImage: "pencil") 42 | .font(.system(size: 15, weight: .bold)) 43 | .labelStyle(IconOnlyLabelStyle()) 44 | .imageScale(.small) 45 | .aspectRatio(1.0, contentMode: .fit) 46 | .cornerRadius(4) 47 | } 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Circuit.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public struct Circuit { 4 | public var rows: Int { 5 | didSet { 6 | setZIndex() 7 | setGridIndex() 8 | } 9 | } 10 | public var columns: Int { 11 | didSet { 12 | setZIndex() 13 | setGridIndex() 14 | } 15 | } 16 | 17 | let minRows: Int 18 | let minColumns: Int 19 | 20 | public var xRange: Range { 0.. { 0.. , yR: Range) -> Grid { 36 | var emptyGrid: Grid = Grid() 37 | for _ in yR { 38 | var emptyRow: Row = Row() 39 | for _ in xR { 40 | emptyRow.append(Slot(component: BlankSpace())) 41 | } 42 | emptyGrid.append(emptyRow) 43 | } 44 | return emptyGrid 45 | } 46 | 47 | mutating func setZIndex() { 48 | let xCount = columns 49 | 50 | for y in yRange{ 51 | for x in xRange { 52 | circuitGrid.rows[y].slots[x].zIndex = y * xCount + (xCount - x) 53 | } 54 | } 55 | } 56 | 57 | mutating func setGridIndex() { 58 | var zArray = [Int]() 59 | let xCount = columns 60 | 61 | for y in yRange{ 62 | for x in xRange { 63 | circuitGrid.rows[y].slots[x].gridIndex = (x: x, y: y) 64 | } 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/Chapters/CircuitPlay.playgroundchapter/Pages/Ohm's Law.playgroundpage/main.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Ohm's Law 3 | This page features a simple circuit with two components: a voltage source and a resistor, which will show how the relationship of the voltage to the resistance and current. This relationship is described by the Ohm’s Law, which states that the voltage equal to the product of the resistance and current. 4 | 5 | ![Ohm's Law Equation](OhmsLaw.PNG) 6 | 7 | **Task:** Ohm My Law! 8 | 1. Tap `Run My Code` button on the right. 9 | 2. Click on the `Solve` button. Feel free to tap on any of the components to check the values they hold. Components like the voltage source and resistor will show their voltage, current, and power values while wires will show their voltage in respect to the ground. The arrows indicate the current direction while the positive and negative signs indicate where the higher and lower voltages are. 10 | 3. With a `5.00V` voltage source and `10.00Ω` resistor, the current across the voltage source is 500mA. You may change values of each components with the Setup button and tapping on the value you want to change. 11 | 4. What happens when we increase the resistance value of the resistor? What happens to the current? What happens when we increase the value of the voltage? 12 | 5. Click on the `Design` button and replace the voltage source with a current source and solve for the circuit? What is the current across the resistor? What about voltages of the components? 13 | 6. On the `Setup` button, you may also view your circuits in augmented reality. In this mode, you may also tap on each model to view their values. 14 | 15 | 16 | Go to the [Next Page](@next) where you will learn about resistors in series. 17 | */ 18 | import SwiftUI 19 | import PlaygroundSupport 20 | 21 | let preset: CircuitPreset = .ohmsLaw 22 | PlaygroundPage.current.wantsFullScreenLiveView = true 23 | PlaygroundPage.current.needsIndefiniteExecution = true 24 | PlaygroundPage.current.setLiveView( 25 | MainView() 26 | .environmentObject(CircuitEnvironment(circuit: preset.circuit)) 27 | .ignoresSafeArea() 28 | ) 29 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/StatePickerView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct StatePicker: View { 4 | @EnvironmentObject var envi: CircuitEnvironment 5 | 6 | public var body: some View { 7 | GeometryReader { geo in 8 | ZStack{ 9 | RoundedRectangle(cornerRadius: 8) 10 | .fill(Color.blue.opacity(0.5)) 11 | .offset(x: envi.currentState == .design ? -88 : 0) 12 | .offset(x: envi.currentState == .play ? 88 : 0) 13 | .frame(width: 80, height: 40, alignment: .center) 14 | HStack{ 15 | Button(action: {envi.currentState = .design} ) { 16 | Text("DESIGN") 17 | .fontWeight(.heavy) 18 | }.buttonStyle(PickerButtonStyle()) 19 | Button(action: {envi.currentState = .setup} ) { 20 | Text("SETUP") 21 | .fontWeight(.heavy) 22 | }.buttonStyle(PickerButtonStyle()) 23 | Button(action: {envi.solveCircuit()} ) { 24 | Text("SOLVE") 25 | .fontWeight(.heavy) 26 | }.buttonStyle(PickerButtonStyle()) 27 | }.padding(8) 28 | }.background(BlurView()) 29 | .background(Color.gray.opacity(0.25)) 30 | .cornerRadius(12) 31 | .position(x: geo.frame(in: .global).midX, y: geo.frame(in: .global).minY + 120) 32 | }.animation(.spring(), value: envi.currentState) 33 | } 34 | } 35 | 36 | struct PickerButtonStyle: ButtonStyle { 37 | func makeBody(configuration: Configuration) -> some View { 38 | configuration.label 39 | .frame(width: 72, height: 32, alignment: .center) 40 | .aspectRatio(1.0, contentMode: .fit) 41 | .scaleEffect(configuration.isPressed ? 1.0 : 1.0) 42 | .shadow(radius: configuration.isPressed ? 7.5 : 0) 43 | .animation(.linear, value: configuration.isPressed) 44 | .padding(4) 45 | .foregroundColor(Color.primary) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/SideButtonsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct DesignSideButtonsView: View { 5 | @EnvironmentObject var envi: CircuitEnvironment 6 | 7 | public var body: some View { 8 | VStack(spacing: 0) { 9 | Button(action: { 10 | guard let selectedSlot = envi.selectedSlot else { return } 11 | envi.circuit.circuitGrid[selectedSlot].component.rotate(.clockwise) 12 | }){ 13 | Label("Rotate Right", systemImage: "rotate.right") 14 | .labelStyle(IconOnlyLabelStyle()) 15 | }.buttonStyle(SideButtonStyle()) 16 | Button(action: { 17 | guard let selectedSlot = envi.selectedSlot else { return } 18 | envi.circuit.circuitGrid[selectedSlot].component.rotate(.counterclockwise) 19 | }){ 20 | Label("Rotate Left", systemImage: "rotate.left") 21 | .labelStyle(IconOnlyLabelStyle()) 22 | }.buttonStyle(SideButtonStyle()) 23 | Button(action: { 24 | guard let selectedSlot = envi.selectedSlot else { return } 25 | envi.circuit.circuitGrid[selectedSlot].component = BlankSpace() 26 | envi.selectedSlot = nil 27 | }){ 28 | Label("Trash", systemImage: "trash") 29 | .labelStyle(IconOnlyLabelStyle()) 30 | }.buttonStyle(SideButtonStyle()) 31 | }.background(BlurView()) 32 | .cornerRadius(8) 33 | } 34 | } 35 | 36 | public struct SideButtonStyle: ButtonStyle { 37 | public init(){} 38 | public func makeBody(configuration: Configuration) -> some View { 39 | configuration.label 40 | .scaleEffect(configuration.isPressed ? 1.25 : 1.0) 41 | .frame(width: 24, height: 24, alignment: .center) 42 | .aspectRatio(1.0, contentMode: .fit) 43 | .padding(4) 44 | .background(configuration.isPressed ? Color.blue.opacity(0.5) : Color.gray.opacity(0.25)) 45 | .shadow(radius: configuration.isPressed ? 7.5 : 0) 46 | .animation(.linear, value: configuration.isPressed) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Circuit+Debug.swift: -------------------------------------------------------------------------------- 1 | extension Circuit { 2 | public func debugLayout() { 3 | for row in circuitGrid.rows{ 4 | var rowStr: String = "" 5 | for comp in row.slots { 6 | rowStr.append(comp.description) 7 | rowStr.append(" ") 8 | } 9 | print(rowStr) 10 | } 11 | } 12 | 13 | public mutating func stringGridTranslation(_ grid: [[String]]) { 14 | let translationGrid = grid.map({$0.map({$0.translateToComponent()})}) 15 | for y in yRange { 16 | for x in xRange { 17 | circuitGrid.rows[y].slots[x].changeComponent(to: translationGrid[y][x]) 18 | } 19 | } 20 | } 21 | } 22 | 23 | extension String { 24 | public func translateToComponent() -> CircuitComponent{ 25 | switch self { 26 | case "│": 27 | return Wire(.up, .down) 28 | case "─": 29 | return Wire(.right, .left) 30 | case "┘": 31 | return Wire(.up, .left) 32 | case "└": 33 | return Wire(.up, .right) 34 | case "┐": 35 | return Wire(.left, .down) 36 | case "┌": 37 | return Wire(.right, .down) 38 | case "┴": 39 | return Wire(.up, .left, .right) 40 | case "┬": 41 | return Wire(.down, .right, .left) 42 | case "┤": 43 | return Wire(.up, .down, .left) 44 | case "├": 45 | return Wire(.up, .down, .right) 46 | case "┼": 47 | return Wire(.up, .down, .left, .right) 48 | case "R": 49 | return Resistor(.left, .right) 50 | case "V": 51 | return Battery(positive: .right, negative: .left) 52 | case "G": 53 | return Ground(.right) 54 | case "D": 55 | return Diode(positive: .right, negative: .left) 56 | case "L": 57 | return Inductor(.right, .left) 58 | case "C": 59 | return Capacitor(.right, .left) 60 | case "I": 61 | return CurrentSource(positive: .right, negative: .left) 62 | case "S": 63 | return VoltageSource(positive: .right, negative: .left) 64 | default: 65 | return BlankSpace() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/UnitEnums.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | public enum Dimension: String, Identifiable { 4 | case voltage 5 | case resistance 6 | case current 7 | case power 8 | case frequency 9 | case inductance 10 | case capacitance 11 | 12 | public var id: String { 13 | self.rawValue.capitalized 14 | } 15 | } 16 | 17 | public enum UnitPrefix: Int { 18 | case tera = 12 19 | case giga = 9 20 | case mega = 6 21 | case kilo = 3 22 | case base = 0 23 | case milli = -3 24 | case micro = -6 25 | case nano = -9 26 | case pico = -12 27 | 28 | public var symbol: String { 29 | switch self { 30 | case .tera: 31 | return "T" 32 | case .giga: 33 | return "G" 34 | case .mega: 35 | return "M" 36 | case .kilo: 37 | return "k" 38 | case .base: 39 | return "" 40 | case .milli: 41 | return "m" 42 | case .micro: 43 | return "μ" 44 | case .nano: 45 | return "n" 46 | case .pico: 47 | return "p" 48 | } 49 | } 50 | } 51 | 52 | public enum BaseUnit { 53 | case volt 54 | case ohm 55 | case ampere 56 | case watt 57 | case hertz 58 | case henry 59 | case farad 60 | 61 | public var symbol: String { 62 | switch self { 63 | case .volt: 64 | return "V" 65 | case .ampere: 66 | return "A" 67 | case .ohm: 68 | return "Ω" 69 | case .watt: 70 | return "W" 71 | case .hertz: 72 | return "Hz" 73 | case .henry: 74 | return "H" 75 | case .farad: 76 | return "F" 77 | } 78 | } 79 | 80 | public func createUnit(from value: Double) -> CircuitUnit { 81 | switch self { 82 | case .volt: 83 | return value.volts() 84 | case .ampere: 85 | return value.amperes() 86 | case .ohm: 87 | return value.ohms() 88 | case .watt: 89 | return value.watts() 90 | case .hertz: 91 | return value.hertz() 92 | case .henry: 93 | return value.henries() 94 | case .farad: 95 | return value.farads() 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Presets.swift: -------------------------------------------------------------------------------- 1 | public enum CircuitPreset { 2 | case blank 3 | case ohmsLaw 4 | case series 5 | case parallel 6 | case resistorCapacitor 7 | case resistorInductor 8 | 9 | public var circuit: Circuit { 10 | switch self { 11 | case .blank: 12 | var blankCircuit = Circuit(3, 3) 13 | return blankCircuit 14 | case .ohmsLaw: 15 | let circuit: [[String]] = [["G","R","┐"], 16 | [" "," ","│",], 17 | ["G","V","┘"]] 18 | 19 | var translatedCircuit = Circuit(3, 3) 20 | translatedCircuit.stringGridTranslation(circuit) 21 | return translatedCircuit 22 | case .series: 23 | let circuit: [[String]] = [["G","R","┐"], 24 | [" "," ","R",], 25 | ["G","V","┘"]] 26 | 27 | var translatedCircuit = Circuit(3, 3) 28 | translatedCircuit.stringGridTranslation(circuit) 29 | translatedCircuit.circuitGrid[(2,1)].component.rotate(.clockwise) 30 | return translatedCircuit 31 | case .parallel: 32 | let circuit: [[String]] = [["G","R","┐"], 33 | ["G","R","┤"], 34 | ["G","V","┘"]] 35 | 36 | var translatedCircuit = Circuit(3, 3) 37 | translatedCircuit.stringGridTranslation(circuit) 38 | return translatedCircuit 39 | case .resistorCapacitor: 40 | let circuit: [[String]] = [["G","C","┐"], 41 | [" "," ","R",], 42 | ["G","V","┘"]] 43 | 44 | var translatedCircuit = Circuit(3, 3) 45 | translatedCircuit.stringGridTranslation(circuit) 46 | translatedCircuit.circuitGrid[(2,1)].component.rotate(.clockwise) 47 | return translatedCircuit 48 | case .resistorInductor: 49 | let circuit: [[String]] = [["G","L","┐"], 50 | [" "," ","R",], 51 | ["G","V","┘"]] 52 | 53 | var translatedCircuit = Circuit(3, 3) 54 | translatedCircuit.stringGridTranslation(circuit) 55 | translatedCircuit.circuitGrid[(2,1)].component.rotate(.clockwise) 56 | return translatedCircuit 57 | } 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Circuit+Navigation.swift: -------------------------------------------------------------------------------- 1 | extension Circuit { 2 | public mutating func shiftGrid(going direction: Direction){ 3 | func createRepeatingBlankSlots(_ count: Int) -> [Slot] { 4 | var blankSlots = [Slot]() 5 | for _ in 0.. Location { 53 | var newLocation: Location { 54 | switch direction { 55 | case .up: 56 | return (location.x, location.y - 1) 57 | case .down: 58 | return (location.x, location.y + 1) 59 | case .left: 60 | return (location.x - 1, location.y) 61 | case .right: 62 | return (location.x + 1, location.y) 63 | } 64 | } 65 | 66 | guard newLocation.x >= 0, 67 | newLocation.y >= 0, 68 | newLocation.x < self.columns, 69 | newLocation.y < self.rows else { 70 | throw CircuitError.LocationOutOfBoundsError 71 | } 72 | 73 | return newLocation 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/MainView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | import CircuitReality 4 | 5 | public struct MainView: View { 6 | @EnvironmentObject var envi: CircuitEnvironment 7 | 8 | public init(){} 9 | 10 | public var body: some View { 11 | ZStack { 12 | if envi.currentState == .play && envi.arMode { 13 | ARViewContainer() 14 | } else { 15 | GridView() 16 | } 17 | StatePicker() 18 | if envi.currentState == .play { 19 | InterfacePicker() 20 | } 21 | DockView() 22 | .opacity(envi.currentState == .design ? 1.0 : 0.0) 23 | .allowsHitTesting(envi.currentState == .design) 24 | .offset(y: envi.currentState == .design ? 0.0 : 120) 25 | .animation(.spring(), value: envi.currentState) 26 | if let position = envi.dragPosition { 27 | HoverView() 28 | .position(position) 29 | } 30 | if envi.currentState == .play { 31 | InfoView() 32 | NextStepView() 33 | } 34 | }.animation(.linear, value: envi.currentState) 35 | } 36 | } 37 | 38 | public struct HoverView: View { 39 | @EnvironmentObject var envi: CircuitEnvironment 40 | 41 | public init(){} 42 | 43 | public var body: some View { 44 | if envi.firstDragIndex == nil, let draggedDock = envi.draggedDock { 45 | ComponentView(component: draggedDock) 46 | .frame(width: 72, height: 72, alignment: .center) 47 | .background(BlurView()) 48 | .cornerRadius(8) 49 | .overlay( 50 | RoundedRectangle(cornerRadius: 8) 51 | .stroke(envi.dragState == .valid ? Color.green : Color.red, lineWidth: 4) 52 | ) 53 | .shadow(radius: 8) 54 | .animation(.linear(duration: 0.1875), value: envi.dragState) 55 | } 56 | if let firstDragIndex = envi.firstDragIndex { 57 | ComponentView(component: envi.circuit.circuitGrid[firstDragIndex].component) 58 | .frame(width: 72, height: 72, alignment: .center) 59 | .background(BlurView()) 60 | .cornerRadius(8) 61 | .overlay( 62 | RoundedRectangle(cornerRadius: 8) 63 | .stroke(envi.dragState == .valid ? Color.green : Color.red, lineWidth: 4) 64 | ) 65 | .shadow(radius: 8) 66 | .animation(.linear(duration: 0.1875), value: envi.dragState) 67 | } 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/ArrowsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct ArrowsView: View { 5 | private var direction: Direction 6 | private var rotationCount: Double 7 | private var isNegative: Bool 8 | private var isFlipped: Bool { direction == .right || direction == .down} 9 | public init(direction: Direction, rotationCount: Double, isNegative: Bool) { 10 | self.direction = direction 11 | self.rotationCount = rotationCount 12 | self.isNegative = isNegative 13 | } 14 | 15 | public var body: some View { 16 | GeometryReader{ geometry in 17 | SquareImageView("Arrow") 18 | .rotation3DEffect(Angle(degrees: isNegative ? 180 : 0), axis: (x: 0, y: 1, z: 0)) 19 | .rotation3DEffect(isFlipped ? 20 | Angle(degrees: 180) : 21 | Angle(degrees: 0), axis: (x: 1, y: 0, z: 0)) 22 | .rotation3DEffect(Angle(degrees: rotationCount * 90), axis: (x: 0, y: 0, z: 1)) 23 | .frame(width: geometry.size.width / 8, height: geometry.size.height / 8) 24 | .position(ArrowOffset(geometry: geometry, direction: direction).offset) 25 | SquareImageView("Arrow") 26 | .rotation3DEffect(Angle(degrees: isNegative ? 180 : 0), axis: (x: 0, y: 1, z: 0)) 27 | .rotation3DEffect(isFlipped ? 28 | Angle(degrees: 180) : 29 | Angle(degrees: 0), axis: (x: 1, y: 0, z: 0)) 30 | .rotation3DEffect(Angle(degrees: rotationCount * 90), axis: (x: 0, y: 0, z: 1)) 31 | .frame(width: geometry.size.width / 8, height: geometry.size.height / 8) 32 | .position(ArrowOffset(geometry: geometry, direction: direction.opposite).offset) 33 | } 34 | } 35 | } 36 | 37 | private struct ArrowOffset { 38 | private var geometry: GeometryProxy 39 | private var direction: Direction 40 | public var offset: CGPoint { 41 | switch direction { 42 | case .down: 43 | return CGPoint(x: 5 * geometry.size.height / 8, y: 7 * geometry.size.width / 8) 44 | case .up: 45 | return CGPoint(x: 5 * geometry.size.height / 8, y: geometry.size.width / 8) 46 | case .left: 47 | return CGPoint(x: 1 * geometry.size.width / 8, y: 5 * geometry.size.height / 8) 48 | case .right: 49 | return CGPoint(x: 7 * geometry.size.width / 8, y: 5 * geometry.size.height / 8) 50 | } 51 | } 52 | 53 | public init(geometry: GeometryProxy, direction: Direction) { 54 | self.geometry = geometry 55 | self.direction = direction 56 | } 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/FocusEntity.playgroundmodule/Sources/FocusEntityComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusEntity.swift 3 | // FocusEntity 4 | // 5 | // Created by Max Cobb on 8/26/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | #if canImport(ARKit) 10 | import RealityKit 11 | import ARKit 12 | 13 | internal struct ClassicStyle { 14 | var color: Material.Color 15 | } 16 | 17 | /// When using colored style, first material of a mesh will be replaced with the chosen color 18 | internal struct ColoredStyle { 19 | /// Color when tracking the surface of a known plane 20 | var onColor: MaterialColorParameter 21 | /// Color when tracking an estimated plane 22 | var offColor: MaterialColorParameter 23 | /// Color when no surface tracking is achieved 24 | var nonTrackingColor: MaterialColorParameter 25 | var mesh: MeshResource 26 | } 27 | 28 | public struct FocusEntityComponent: Component { 29 | public enum Style { 30 | case classic(color: Material.Color) 31 | case colored( 32 | onColor: MaterialColorParameter, 33 | offColor: MaterialColorParameter, 34 | nonTrackingColor: MaterialColorParameter, 35 | mesh: MeshResource = MeshResource.generatePlane(width: 0.1, depth: 0.1) 36 | ) 37 | } 38 | 39 | let style: Style 40 | var classicStyle: ClassicStyle? { 41 | switch self.style { 42 | case .classic(let color): 43 | return ClassicStyle(color: color) 44 | default: 45 | return nil 46 | } 47 | } 48 | 49 | var coloredStyle: ColoredStyle? { 50 | switch self.style { 51 | case .colored(let onColor, let offColor, let nonTrackingColor, let mesh): 52 | return ColoredStyle( 53 | onColor: onColor, offColor: offColor, 54 | nonTrackingColor: nonTrackingColor, mesh: mesh 55 | ) 56 | default: 57 | return nil 58 | } 59 | } 60 | 61 | /// Convenient presets 62 | public static let classic = FocusEntityComponent(style: .classic(color: #colorLiteral(red: 1, green: 0.8, blue: 0, alpha: 1))) 63 | public static let plane = FocusEntityComponent( 64 | style: .colored( 65 | onColor: .color(.green), 66 | offColor: .color(.orange), 67 | nonTrackingColor: .color(Material.Color.red.withAlphaComponent(0.2)), 68 | mesh: FocusEntityComponent.defaultPlane 69 | ) 70 | ) 71 | internal var isOpen = true 72 | internal var segments: [FocusEntity.Segment] = [] 73 | public var allowedRaycast: ARRaycastQuery.Target = .estimatedPlane 74 | 75 | static var defaultPlane = MeshResource.generatePlane( 76 | width: 0.1, depth: 0.1 77 | ) 78 | 79 | public init(style: Style) { 80 | self.style = style 81 | // If the device has LiDAR, then default behaviour is to only allow 82 | // existing detected planes 83 | if #available(iOS 13.4, *), 84 | ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { 85 | self.allowedRaycast = .existingPlaneGeometry 86 | } 87 | } 88 | } 89 | #endif 90 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/LocationEnums.swift: -------------------------------------------------------------------------------- 1 | public enum Direction: String, Identifiable, Hashable { 2 | case right, left, up, down 3 | 4 | public var id: String { 5 | self.rawValue.capitalized 6 | } 7 | 8 | public var int: Int { 9 | switch self { 10 | case .right: 11 | return 0 12 | case .left: 13 | return 2 14 | case .up: 15 | return 3 16 | case .down: 17 | return 1 18 | } 19 | } 20 | 21 | public var symbol: String { 22 | switch self { 23 | case .right: 24 | return "arrow.right" 25 | case .left: 26 | return "arrow.left" 27 | case .up: 28 | return "arrow.up" 29 | case .down: 30 | return "arrow.down" 31 | } 32 | } 33 | 34 | public var chevron: String { 35 | switch self { 36 | case .right: 37 | return "chevron.right" 38 | case .left: 39 | return "chevron.left" 40 | case .up: 41 | return "chevron.up" 42 | case .down: 43 | return "chevron.down" 44 | } 45 | } 46 | 47 | public var rotationClockwise: Direction { 48 | switch self { 49 | case .right: 50 | return .down 51 | case .left: 52 | return .up 53 | case .up: 54 | return .right 55 | case .down: 56 | return .left 57 | } 58 | } 59 | 60 | public var rotationCounterclockwise: Direction { 61 | switch self { 62 | case .right: 63 | return .up 64 | case .left: 65 | return .down 66 | case .up: 67 | return .left 68 | case .down: 69 | return .right 70 | } 71 | } 72 | 73 | public var flipHorizontal: Direction { 74 | switch self { 75 | case .right: 76 | return .left 77 | case .left: 78 | return .right 79 | case .up: 80 | return .up 81 | case .down: 82 | return .down 83 | } 84 | } 85 | 86 | public var flipVertical: Direction { 87 | switch self { 88 | case .right: 89 | return .right 90 | case .left: 91 | return .left 92 | case .up: 93 | return .down 94 | case .down: 95 | return .up 96 | } 97 | } 98 | 99 | public var opposite: Direction { 100 | switch self { 101 | case .right: 102 | return .left 103 | case .left: 104 | return .right 105 | case .up: 106 | return .down 107 | case .down: 108 | return .up 109 | } 110 | } 111 | } 112 | 113 | public enum Rotation { 114 | case clockwise, counterclockwise 115 | } 116 | 117 | public enum Orientation { 118 | case vertical, horizontal 119 | } 120 | 121 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/WiredComponentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct WiredComponentView: View { 5 | @Environment(\.accessibilityReduceMotion) var reduceMotion 6 | @Environment (\.colorScheme) var colorScheme: ColorScheme 7 | 8 | private var component: Connectable 9 | 10 | private var rotationCount: Double { component.rotationCount } 11 | 12 | private var isFlipped: Bool { 13 | if let terminalComponent = component as? TerminalDependent { 14 | return terminalComponent.terminals.positive == .down || terminalComponent.terminals.positive == .right 15 | } 16 | return [-1.0,-2.0,2.0,3.0].contains(rotationCount.remainder(dividingBy: 4)) 17 | } 18 | private var flippable: Bool { component.componentType.imageName != "Source" } 19 | 20 | public init(component: Connectable){ 21 | self.component = component 22 | } 23 | 24 | public var body: some View { 25 | ZStack { 26 | CondensedWireShape(rotationCount: rotationCount) 27 | SquareImageView(component.componentType.imageName) 28 | .rotation3DEffect(isFlipped && flippable ? 29 | Angle(degrees: 180) : 30 | Angle(degrees: 0), axis: (x: 1, y: 0, z: 0)) 31 | .rotation3DEffect(Angle(degrees: flippable ? rotationCount * 90 : 0), axis: (x: 0, y: 0, z: 1)) 32 | if let currentSource = component as? CurrentSource { 33 | var current = currentSource.measurements[0].value 34 | if current != 0 { 35 | ArrowsView(direction: currentSource.terminals.positive, rotationCount: currentSource.rotationCount, isNegative: current > 0) 36 | } 37 | } 38 | if let terminalComponent = component as? TerminalDependent { 39 | SignsView(positiveDirection: terminalComponent.terminals.positive) 40 | } 41 | } 42 | .aspectRatio(1, contentMode: .fit) 43 | } 44 | } 45 | 46 | struct CondensedWireShape: View { 47 | private var rotationCount: Double 48 | private var rotatingRemainder: Double { rotationCount.truncatingRemainder(dividingBy: 2.0) } 49 | 50 | init(rotationCount: Double) { 51 | self.rotationCount = rotationCount 52 | } 53 | var body: some View { 54 | ZStack { 55 | mainShape 56 | .overlay( 57 | overlay 58 | .mask( 59 | mainShape 60 | ) 61 | ) 62 | .mask(Rectangle()) 63 | } 64 | } 65 | 66 | private var mainShape: some View { 67 | SquareImageView("WireCondensedShape") 68 | .rotation3DEffect( Angle(degrees: rotationCount * 90), 69 | axis: (x: 0, y: 0, z: 1)) 70 | .scaleEffect(1.5) 71 | } 72 | 73 | private var overlay: some View { 74 | Group { 75 | switch rotatingRemainder { 76 | case 0: 77 | SquareImageView("WireStraight") 78 | default: 79 | SquareImageView("WireStraight") 80 | .rotation3DEffect( Angle(degrees: 90), 81 | axis: (x: 0, y: 0, z: 1)) 82 | } 83 | } 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/ARContainerView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import RealityKit 3 | import ARKit 4 | import CircuitKit 5 | import CircuitNetwork 6 | import FocusEntity 7 | import CircuitReality 8 | 9 | protocol CircuitDelegate { 10 | func slotTapped(location: Location) 11 | } 12 | 13 | public struct ARViewContainer: UIViewRepresentable, CircuitDelegate { 14 | @EnvironmentObject var envi: CircuitEnvironment 15 | public init(){} 16 | public func makeUIView(context: Context) -> CircuitARView { 17 | let arView = CircuitARView(frame: .zero, delegate: self, network: envi.network!) 18 | arView.focusEntity?.isEnabled = true 19 | arView.contentScaleFactor = 1 20 | return arView 21 | } 22 | public func updateUIView(_ uiView: CircuitARView, context: Context) { 23 | } 24 | func slotTapped(location: Location) { 25 | envi.selectedSlot = location 26 | } 27 | } 28 | 29 | public class CircuitARView: ARView { 30 | var focusEntity: FocusEntity? 31 | var delegate: CircuitDelegate? 32 | var network: CircuitNetwork? 33 | var objectPlaced = false 34 | 35 | required init(frame frameRect: CGRect, delegate: CircuitDelegate, network: CircuitNetwork) { 36 | super.init(frame: frameRect) 37 | focusEntity = FocusEntity(on: self, focus: .classic) 38 | self.network = network 39 | self.cameraMode = .ar 40 | self.delegate = delegate 41 | self.renderOptions = [.disableFaceOcclusions, .disablePersonOcclusion, .disableHDR] 42 | configure() 43 | enablePlacement() 44 | } 45 | 46 | @objc override required dynamic init(frame frameRect: CGRect) { 47 | fatalError("init(frame:) has not been implemented") 48 | } 49 | 50 | @objc required dynamic init?(coder decoder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | private func configure() { 55 | let config = ARWorldTrackingConfiguration() 56 | config.planeDetection = .horizontal 57 | config.environmentTexturing = .automatic 58 | config.wantsHDREnvironmentTextures = false 59 | session.run(config) 60 | } 61 | func enablePlacement() { 62 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))) 63 | self.addGestureRecognizer(tapGestureRecognizer) 64 | } 65 | @objc func handleTap(recognizer: UITapGestureRecognizer) { 66 | if !objectPlaced { 67 | do { 68 | let reality = CircuitReality(network: network!) 69 | let circuit = reality.makeAnchorEntityForNetwork(position: focusEntity?.position ?? .zero) 70 | self.scene.addAnchor(circuit) 71 | objectPlaced = true 72 | focusEntity?.isEnabled = false 73 | } catch { 74 | print("No model") 75 | } 76 | } else { 77 | let location = recognizer.location(in: self) 78 | let hitTests = hitTest(location, query: .nearest) 79 | for hit in hitTests { 80 | if let locStr = hit.entity.parent?.name.components(separatedBy: ","), let x = Int(locStr[0]), let y = Int(locStr[1]) { 81 | delegate?.slotTapped(location: (x, y)) 82 | } 83 | if let locStr = hit.entity.parent?.parent?.name.components(separatedBy: ","), let x = Int(locStr[0]), let y = Int(locStr[1]) { 84 | delegate?.slotTapped(location: (x, y)) 85 | } 86 | if let locStr = hit.entity.parent?.parent?.parent?.name.components(separatedBy: ","), let x = Int(locStr[0]), let y = Int(locStr[1]) { 87 | delegate?.slotTapped(location: (x, y)) 88 | } 89 | if let locStr = hit.entity.parent?.parent?.parent?.parent?.name.components(separatedBy: ","), let x = Int(locStr[0]), let y = Int(locStr[1]) { 90 | delegate?.slotTapped(location: (x, y)) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/FocusEntity.playgroundmodule/Sources/FocusEntity+Classic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusEntity+Classic.swift 3 | // FocusEntity 4 | // 5 | // Created by Max Cobb on 8/28/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | #if canImport(ARKit) 10 | import RealityKit 11 | 12 | /// An extension of FocusEntity holding the methods for the "classic" style. 13 | internal extension FocusEntity { 14 | 15 | // MARK: - Configuration Properties 16 | 17 | /// Original size of the focus square in meters. Not currently customizable 18 | static let size: Float = 0.17 19 | 20 | /// Thickness of the focus square lines in meters. Not currently customizable 21 | static let thickness: Float = 0.018 22 | 23 | /// Scale factor for the focus square when it is closed, w.r.t. the original size. 24 | static let scaleForClosedSquare: Float = 0.97 25 | 26 | /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). 27 | // static let sideLengthForOpenSegments: CGFloat = 0.2 28 | 29 | /// Duration of the open/close animation. Not currently used. 30 | static let animationDuration = 0.7 31 | 32 | /// Color of the focus square fill. Not currently used. 33 | // static var fillColor = #colorLiteral(red: 1, green: 0.9254901961, blue: 0.4117647059, alpha: 1) 34 | 35 | /// Indicates whether the segments of the focus square are disconnected. 36 | // private var isOpen = true 37 | 38 | /// List of the segments in the focus square. 39 | 40 | // MARK: - Initialization 41 | 42 | func setupClassic(_ classicStyle: ClassicStyle) { 43 | // opacity = 0.0 44 | /* 45 | The focus square consists of eight segments as follows, which can be individually animated. 46 | 47 | s0 s1 48 | _ _ 49 | s2 | | s3 50 | 51 | s4 | | s5 52 | - - 53 | s6 s7 54 | */ 55 | 56 | let segCorners: [(Corner, Alignment)] = [ 57 | (.topLeft, .horizontal), (.topRight, .horizontal), 58 | (.topLeft, .vertical), (.topRight, .vertical), 59 | (.bottomLeft, .vertical), (.bottomRight, .vertical), 60 | (.bottomLeft, .horizontal), (.bottomRight, .horizontal) 61 | ] 62 | self.segments = segCorners.enumerated().map { (index, cornerAlign) -> Segment in 63 | Segment( 64 | name: "s\(index)", 65 | corner: cornerAlign.0, 66 | alignment: cornerAlign.1, 67 | color: classicStyle.color 68 | ) 69 | } 70 | 71 | let sl: Float = 0.5 // segment length 72 | let c: Float = FocusEntity.thickness / 2 // correction to align lines perfectly 73 | segments[0].position += [-(sl / 2 - c), 0, -(sl - c)] 74 | segments[1].position += [sl / 2 - c, 0, -(sl - c)] 75 | segments[2].position += [-sl, 0, -sl / 2] 76 | segments[3].position += [sl, 0, -sl / 2] 77 | segments[4].position += [-sl, 0, sl / 2] 78 | segments[5].position += [sl, 0, sl / 2] 79 | segments[6].position += [-(sl / 2 - c), 0, sl - c] 80 | segments[7].position += [sl / 2 - c, 0, sl - c] 81 | 82 | for segment in segments { 83 | self.positioningEntity.addChild(segment) 84 | segment.open() 85 | } 86 | 87 | // self.positioningEntity.addChild(fillPlane) 88 | self.positioningEntity.scale = SIMD3(repeating: FocusEntity.size * FocusEntity.scaleForClosedSquare) 89 | 90 | // Always render focus square on top of other content. 91 | // self.displayNodeHierarchyOnTop(true) 92 | } 93 | 94 | // MARK: Animations 95 | 96 | func offPlaneAniation() { 97 | // Open animation 98 | guard !isOpen else { 99 | return 100 | } 101 | isOpen = true 102 | 103 | for segment in segments { 104 | segment.open() 105 | } 106 | positioningEntity.scale = .init(repeating: FocusEntity.size) 107 | } 108 | 109 | func onPlaneAnimation(newPlane: Bool = false) { 110 | guard isOpen else { 111 | return 112 | } 113 | self.isOpen = false 114 | 115 | // Close animation 116 | for segment in self.segments { 117 | segment.close() 118 | } 119 | 120 | if newPlane { 121 | // New plane animation not implemented 122 | } 123 | } 124 | 125 | } 126 | #endif 127 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/Environment.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SwiftUI 3 | import CircuitKit 4 | import CircuitNetwork 5 | import RealityKit 6 | import ARKit 7 | 8 | public class CircuitEnvironment: ObservableObject { 9 | @Published public var circuit: Circuit 10 | 11 | @Published public var network: CircuitNetwork? 12 | 13 | @Published public var arMode: Bool = false 14 | 15 | public init(circuit: Circuit){ 16 | self.circuit = circuit 17 | let detector = CurrentValueSubject(.zero) 18 | self.publisher = detector 19 | .debounce(for: .seconds(1/60), scheduler: DispatchQueue.main) 20 | .dropFirst() 21 | .eraseToAnyPublisher() 22 | self.detector = detector 23 | } 24 | 25 | public func solveCircuit() { 26 | network = CircuitNetwork(circuit: circuit) 27 | network?.solveNetwork() 28 | currentState = .play 29 | } 30 | 31 | @Published var currentState: ViewState = .design { 32 | didSet { 33 | selectedSlot = nil 34 | } 35 | } 36 | 37 | let timer: Publishers.Autoconnect = Timer.publish(every: 1 / 60, on: .main, in: .common).autoconnect() 38 | 39 | let detector: CurrentValueSubject 40 | let publisher: AnyPublisher 41 | 42 | @Published public var selectedSlot: Location? { 43 | willSet { 44 | guard let selectedSlot = selectedSlot else { return } 45 | circuit.circuitGrid[(selectedSlot)].selected = false 46 | } 47 | } 48 | 49 | @Published var popoverShown: Bool = false 50 | 51 | @Published var dragPosition: CGPoint? 52 | 53 | @Published var firstDragIndex: Location? 54 | 55 | var dragState: DragState? { 56 | switch (dragPosition, dragGridIndex) { 57 | case (nil, _): 58 | return nil 59 | case (_, nil): 60 | return .unknown 61 | default: 62 | return .valid 63 | } 64 | } 65 | 66 | @Published var gridOrigin: CGPoint = CGPoint.zero { 67 | didSet { 68 | DispatchQueue.main.async { [self] in 69 | dragPosition = nil 70 | } 71 | } 72 | } 73 | 74 | 75 | var gridRect: CGRect { 76 | let gridSize = CGSize(width: circuit.columns * 128, height: circuit.rows * 128) 77 | return CGRect(origin: gridOrigin, size: gridSize) 78 | } 79 | 80 | var dragGridIndex: Location? { 81 | guard let dragPoint = dragPosition, 82 | gridRect.contains(dragPoint) else { return nil } 83 | return pointToGridIndex(point: dragPoint, in: gridRect) 84 | } 85 | 86 | public func pointToGridIndex(point: CGPoint, in rect: CGRect) -> Location? { 87 | let xDiff = point.x - rect.origin.x 88 | let yDiff = point.y - rect.origin.y 89 | 90 | let x: Int = Int(floor(xDiff / 128)) 91 | let y: Int = Int(floor(yDiff / 128)) 92 | 93 | guard x >= 0, y >= 0 else { return nil } 94 | 95 | guard x < circuit.columns, y < circuit.rows else { return nil } 96 | 97 | return (x, y) 98 | } 99 | 100 | @Published var draggedDock: CircuitComponent? 101 | 102 | @Published var dockOrigin: CGFloat = CGFloat.zero { 103 | didSet { 104 | DispatchQueue.main.async { [self] in 105 | dragPosition = nil 106 | } 107 | } 108 | } 109 | 110 | func getModelIndexAt(location: Location) -> Int? { 111 | guard let network = network else { return nil } 112 | let modelSlot = network.circuit.circuitGrid[location] 113 | guard let model = modelSlot.assocModel else { return nil } 114 | return network.models.firstIndex { $0.description == model} 115 | } 116 | 117 | func getNodeIndexAt(location: Location) -> Int? { 118 | guard let network = network else { return nil } 119 | let nodeSlot = network.circuit.circuitGrid[location] 120 | guard let node = nodeSlot.assocNode else { return nil } 121 | return node 122 | } 123 | } 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/DockView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct DockView: View { 5 | @EnvironmentObject var envi: CircuitEnvironment 6 | @State private var dockOrigin: CGFloat = 0 7 | @State private var dockSlots: [Slot] = [Slot(component: Wire(.left, .right)), 8 | Slot(component: Wire(.up, .right)), 9 | Slot(component: Wire(.up, .right, .left)), 10 | Slot(component: Wire(.up, .right, .left, .down)), 11 | Slot(component: Battery(positive: .right, negative: .left)), 12 | Slot(component: Resistor(.left, .right)), 13 | Slot(component: Ground(.up)), 14 | Slot(component: CurrentSource(positive: .right, negative: .left)), 15 | Slot(component: Capacitor(.left, .right)), 16 | Slot(component: Inductor(.left, .right))] 17 | @State var showDock: Bool = true 18 | 19 | public init(){} 20 | public var body: some View { 21 | GeometryReader { geo in 22 | ChevronButton($showDock, direction: .up) 23 | .position(x: showDock ? geo.frame(in: .global).midX - 260 : geo.frame(in: .global).midX, y: showDock ? geo.frame(in: .global).maxY - 270 : geo.frame(in: .global).maxY - 115 24 | ) 25 | ScrollView(.horizontal, showsIndicators: false) { 26 | HStack(alignment: .top,spacing: 16) { 27 | ForEach(dockSlots) { slot in 28 | CardView(slot: slot) 29 | .simultaneousGesture(DragGesture(coordinateSpace: .global) 30 | .onChanged { value in 31 | envi.dragPosition = envi.popoverShown ? nil : value.location 32 | envi.selectedSlot = nil 33 | envi.draggedDock = slot.component 34 | } 35 | .onEnded{ finalValue in 36 | envi.dragPosition = nil 37 | let finalDragIndex = envi.pointToGridIndex(point: finalValue.location, in: envi.gridRect) 38 | guard let finalIndex = finalDragIndex else { return } 39 | envi.circuit.circuitGrid[finalIndex].changeComponent(to: slot.component) 40 | } 41 | ) 42 | .allowsHitTesting(envi.dragPosition == nil) 43 | } 44 | } 45 | .overlay( 46 | GeometryReader { geo in 47 | Color.clear 48 | .onChange(of: geo.frame(in: .global).origin, perform: {value in 49 | DispatchQueue.main.async { 50 | envi.dockOrigin = geo.frame(in: .global).origin.x 51 | } 52 | }) 53 | } 54 | ) 55 | .padding(16) 56 | } 57 | .background(BlurView()) 58 | .background(Color.gray.opacity(0.25)) 59 | .cornerRadius(16) 60 | .padding(16) 61 | .frame(maxWidth: 600, alignment: .center) 62 | .position(x: geo.frame(in: .global).midX, y: showDock ? geo.frame(in: .global).maxY - 170 : geo.frame(in: .global).maxY + 200) 63 | }.animation(.spring(), value: showDock) 64 | } 65 | } 66 | 67 | public struct CardView: View { 68 | @Environment (\.colorScheme) var colorScheme: ColorScheme 69 | @EnvironmentObject var envi: CircuitEnvironment 70 | var slot: Slot 71 | 72 | public var body: some View { 73 | VStack { 74 | ComponentView(component: slot.component) 75 | .clipShape(RoundedRectangle(cornerRadius: 8)) 76 | .overlay(RoundedRectangle(cornerRadius: 8) 77 | .stroke(Color.clear.getBackground(for: colorScheme), lineWidth: 4)) 78 | .transition(.opacity) 79 | .frame(width: 72, height: 72, alignment: .center) 80 | Text(slot.component.componentType.name) 81 | .font(.system(size: 15)) 82 | } 83 | } 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Contacts.swift: -------------------------------------------------------------------------------- 1 | public enum ConnectionType { 2 | case single, straight, corner, tshape, cross, none 3 | } 4 | 5 | public struct Contacts: CustomStringConvertible, Equatable { 6 | public var contactPoints: Set 7 | 8 | public var orientation: Orientation { 9 | if contactPoints == Set([.up, .down]) { 10 | return .vertical 11 | } else { 12 | return .horizontal 13 | } 14 | } 15 | 16 | var connectionType: ConnectionType { 17 | switch contactPoints.count { 18 | case 1: 19 | return .single 20 | case 2: 21 | switch contactPoints { 22 | case Set([.right, .left]), Set([.up, .down]): 23 | return .straight 24 | default: 25 | return .corner 26 | } 27 | case 3: 28 | return .tshape 29 | case 4: 30 | return .cross 31 | default: 32 | return .none 33 | } 34 | } 35 | public var description: String { 36 | switch contactPoints { 37 | case Set([.up]): 38 | return "╵" 39 | case Set([.down]): 40 | return "╷" 41 | case Set([.right]): 42 | return "╴" 43 | case Set([.left]): 44 | return "╶" 45 | case Set([.up, .down]): 46 | return "│" 47 | case Set([.right, .left]): 48 | return "─" 49 | case Set([.up, .left]): 50 | return "┘" 51 | case Set([.up, .right]): 52 | return "└" 53 | case Set([.left, .down]): 54 | return "┐" 55 | case Set([.right, .down]): 56 | return "┌" 57 | case Set([.up, .left, .right]): 58 | return "┴" 59 | case Set([.down, .right, .left]): 60 | return "┬" 61 | case Set([.up, .down, .left]): 62 | return "┤" 63 | case Set([.up, .down, .right]): 64 | return "├" 65 | case Set([.up, .down, .left, .right]): 66 | return "┼" 67 | default: 68 | return " " 69 | } 70 | } 71 | 72 | public init(_ contactPoints: [Direction]) { 73 | self.contactPoints = Set(contactPoints) 74 | } 75 | 76 | public mutating func rotate(_ rotation: Rotation) { 77 | switch rotation { 78 | case .clockwise: 79 | self.contactPoints = Set(contactPoints.map({$0.rotationClockwise})) 80 | case .counterclockwise: 81 | self.contactPoints = Set(contactPoints.map({$0.rotationCounterclockwise})) 82 | } 83 | } 84 | 85 | public mutating func flip(_ orientation: Orientation) { 86 | switch orientation { 87 | case .horizontal: 88 | self.contactPoints = Set(contactPoints.map({$0.flipHorizontal})) 89 | case .vertical: 90 | self.contactPoints = Set(contactPoints.map({$0.flipVertical})) 91 | } 92 | } 93 | 94 | public static func == (lhs: Contacts, rhs: Contacts) -> Bool { 95 | return 96 | lhs.contactPoints == rhs.contactPoints 97 | } 98 | 99 | public func getRotationCount(count: inout Double) { 100 | switch connectionType { 101 | case .single: 102 | switch contactPoints { 103 | case Set([.up]): 104 | count = 3 105 | case Set([.right]): 106 | count = 0 107 | case Set([.left]): 108 | count = 1 109 | default: 110 | count = 2 111 | } 112 | case .straight: 113 | switch contactPoints { 114 | case Set([.up, .down]): 115 | count = 1 116 | default: 117 | count = 0 118 | } 119 | case .corner: 120 | switch contactPoints { 121 | case Set([.up, .right]): 122 | count = 1 123 | case Set([.down, .right]): 124 | count = 2 125 | case Set([.down, .left]): 126 | count = 3 127 | default: 128 | count = 0 129 | } 130 | case .tshape: 131 | switch contactPoints { 132 | case Set([.up, .left, .right]): 133 | count = 1 134 | case Set([.down, .up, .right]): 135 | count = 2 136 | case Set([.down, .left, .right]): 137 | count = 3 138 | default: 139 | count = 0 140 | } 141 | default: 142 | count = 0 143 | } 144 | } 145 | } 146 | 147 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/GroundView.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | import SwiftUI 3 | 4 | public struct GroundView: View { 5 | private var ground: Ground 6 | 7 | private var rotationCount: Double { ground.rotationCount } 8 | private var rotatingRemainder: Double { rotationCount.truncatingRemainder(dividingBy: 4.0) } 9 | private var isFlipped: Bool { 10 | [-1.0,-2.0,2.0,3.0].contains(rotationCount.remainder(dividingBy: 4)) 11 | } 12 | 13 | public init(ground: Ground) { 14 | self.ground = ground 15 | } 16 | 17 | public var body: some View { 18 | ZStack { 19 | wireGround 20 | groundNode 21 | } 22 | } 23 | 24 | var wireGround: some View { 25 | WireShape(mask: { 26 | SquareImageView("WireGroundShape") 27 | .rotation3DEffect( Angle(degrees: rotationCount * 90), 28 | axis: (x: 0, y: 0, z: 1)) 29 | .scaleEffect(1.5) 30 | }, overlay: { 31 | switch rotatingRemainder { 32 | case 1, -3: 33 | SquareImageView("WireGround2") 34 | .rotation3DEffect( Angle(degrees: 90), 35 | axis: (x: 0, y: 0, z: 1)) 36 | .rotation3DEffect( Angle(degrees: 180), 37 | axis: (x: 0, y: 1, z: 0)) 38 | case 2, -2: 39 | SquareImageView("WireGround2") 40 | .rotation3DEffect( Angle(degrees: 180), 41 | axis: (x: 0, y: 0, z: 1)) 42 | case 3, -1: 43 | SquareImageView("WireGround1") 44 | .rotation3DEffect( Angle(degrees: -90), 45 | axis: (x: 0, y: 0, z: 1)) 46 | .rotation3DEffect( Angle(degrees: 180), 47 | axis: (x: 0, y: 1, z: 0)) 48 | default: 49 | SquareImageView("WireGround1") 50 | } 51 | }) 52 | } 53 | 54 | var groundNode: some View { 55 | GeometryReader { geo in 56 | ZStack { 57 | SquareImageView("Ground3") 58 | .rotation3DEffect(Angle(degrees: rotationCount * 90), axis: (x: 0, y: 0, z: 1)) 59 | .rotation3DEffect([1, -3, 2, -2].contains(rotatingRemainder) ? .degrees(180) : .zero, axis: (x: 1, y: 0, z: 0)) 60 | .rotation3DEffect([2, -2, 3, -1].contains(rotatingRemainder) ? .degrees(180) : .zero, axis: (x: 0, y: 1, z: 0)) 61 | .offset(getOffsetGround3(geo.size)) 62 | SquareImageView("Ground2") 63 | .rotation3DEffect(Angle(degrees: rotationCount * 90), axis: (x: 0, y: 0, z: 1)) 64 | .rotation3DEffect([1, -3, 2, -2].contains(rotatingRemainder) ? .degrees(180) : .zero, axis: (x: 1, y: 0, z: 0)) 65 | .rotation3DEffect([2, -2, 3, -1].contains(rotatingRemainder) ? .degrees(180) : .zero, axis: (x: 0, y: 1, z: 0)) 66 | SquareImageView("Ground1") 67 | .rotation3DEffect(Angle(degrees: rotationCount * 90), axis: (x: 0, y: 0, z: 1)) 68 | .rotation3DEffect([1, -3, 2, -2].contains(rotatingRemainder) ? .degrees(180) : .zero, axis: (x: 1, y: 0, z: 0)) 69 | .rotation3DEffect([2, -2, 3, -1].contains(rotatingRemainder) ? .degrees(180) : .zero, axis: (x: 0, y: 1, z: 0)) 70 | .offset(getOffsetGround1(geo.size)) 71 | }.position(x: geo.frame(in: .local).midX, y: geo.frame(in: .local).midY) 72 | }.aspectRatio(1,contentMode: .fit ) 73 | } 74 | 75 | func getOffsetGround1(_ size: CGSize) -> CGSize { 76 | switch rotatingRemainder { 77 | case 1, -3: 78 | return CGSize(width: 0, height: -3 * size.height / 32) 79 | case 2, -2: 80 | return CGSize(width: 3 * size.width / 32, height: 0) 81 | case 3, -1: 82 | return CGSize(width: 0, height: 3 * size.height / 32) 83 | default: 84 | return CGSize(width: -3 * size.width / 32, height: 0) 85 | } 86 | } 87 | 88 | func getOffsetGround3(_ size: CGSize) -> CGSize { 89 | switch rotatingRemainder { 90 | case 1, -3: 91 | return CGSize(width: 0, height: 3 * size.height / 32) 92 | case 2, -2: 93 | return CGSize(width: -3 * size.width / 32, height: 0) 94 | case 3, -1: 95 | return CGSize(width: 0, height: -3 * size.height / 32) 96 | default: 97 | return CGSize(width: 3 * size.width / 32, height: 0) 98 | } 99 | } 100 | } 101 | 102 | 103 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/FocusEntity.playgroundmodule/Sources/FocusEntity+Segment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusEntity+Segment.swift 3 | // FocusEntity 4 | // 5 | // Created by Max Cobb on 8/28/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | #if canImport(ARKit) 10 | import RealityKit 11 | 12 | internal extension FocusEntity { 13 | /* 14 | The focus square consists of eight segments as follows, which can be individually animated. 15 | 16 | s0 s1 17 | _ _ 18 | s2 | | s3 19 | 20 | s4 | | s5 21 | - - 22 | s6 s7 23 | */ 24 | enum Corner { 25 | case topLeft // s0, s2 26 | case topRight // s1, s3 27 | case bottomRight // s5, s7 28 | case bottomLeft // s4, s6 29 | } 30 | 31 | enum Alignment { 32 | case horizontal // s0, s1, s6, s7 33 | case vertical // s2, s3, s4, s5 34 | } 35 | 36 | enum Direction { 37 | case up, down, left, right 38 | 39 | var reversed: Direction { 40 | switch self { 41 | case .up: return .down 42 | case .down: return .up 43 | case .left: return .right 44 | case .right: return .left 45 | } 46 | } 47 | } 48 | 49 | class Segment: Entity, HasModel { 50 | 51 | // MARK: - Configuration & Initialization 52 | 53 | /// Thickness of the focus square lines in m. 54 | static let thickness: Float = 0.018 55 | 56 | /// Length of the focus square lines in m. 57 | static let length: Float = 0.5 // segment length 58 | 59 | /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). 60 | static let openLength: Float = 0.2 61 | 62 | let corner: Corner 63 | let alignment: Alignment 64 | let plane: ModelComponent 65 | 66 | init(name: String, corner: Corner, alignment: Alignment, color: Material.Color) { 67 | self.corner = corner 68 | self.alignment = alignment 69 | 70 | switch alignment { 71 | case .vertical: 72 | plane = ModelComponent( 73 | mesh: .generatePlane(width: 1, depth: 1), 74 | materials: [UnlitMaterial(color: color)] 75 | ) 76 | case .horizontal: 77 | plane = ModelComponent( 78 | mesh: .generatePlane(width: 1, depth: 1), 79 | materials: [UnlitMaterial(color: color)] 80 | ) 81 | } 82 | super.init() 83 | 84 | switch alignment { 85 | case .vertical: 86 | self.scale = [Segment.thickness, 1, Segment.length] 87 | case .horizontal: 88 | self.scale = [Segment.length, 1, Segment.thickness] 89 | } 90 | // self.orientation = .init(angle: .pi / 2, axis: [1, 0, 0]) 91 | self.name = name 92 | 93 | // let material = plane.firstMaterial! 94 | // material.diffuse.contents = FocusSquare.primaryColor 95 | // material.isDoubleSided = true 96 | // material.ambient.contents = UIColor.black 97 | // material.lightingModel = .constant 98 | // material.emission.contents = FocusSquare.primaryColor 99 | model = plane 100 | } 101 | 102 | required init() { 103 | fatalError("init() has not been implemented") 104 | } 105 | 106 | // MARK: - Animating Open/Closed 107 | 108 | var openDirection: Direction { 109 | switch (corner, alignment) { 110 | case (.topLeft, .horizontal): return .left 111 | case (.topLeft, .vertical): return .up 112 | case (.topRight, .horizontal): return .right 113 | case (.topRight, .vertical): return .up 114 | case (.bottomLeft, .horizontal): return .left 115 | case (.bottomLeft, .vertical): return .down 116 | case (.bottomRight, .horizontal): return .right 117 | case (.bottomRight, .vertical): return .down 118 | } 119 | } 120 | 121 | func open() { 122 | if alignment == .horizontal { 123 | self.scale[0] = Segment.openLength 124 | } else { 125 | self.scale[2] = Segment.openLength 126 | } 127 | 128 | let offset = Segment.length / 2 - Segment.openLength / 2 129 | updatePosition(withOffset: Float(offset), for: openDirection) 130 | } 131 | 132 | func close() { 133 | let oldLength: Float 134 | if alignment == .horizontal { 135 | oldLength = self.scale[0] 136 | self.scale[0] = Segment.length 137 | } else { 138 | oldLength = self.scale[2] 139 | self.scale[2] = Segment.length 140 | } 141 | 142 | let offset = Segment.length / 2 - oldLength / 2 143 | updatePosition(withOffset: offset, for: openDirection.reversed) 144 | } 145 | 146 | private func updatePosition(withOffset offset: Float, for direction: Direction) { 147 | switch direction { 148 | case .left: position.x -= offset 149 | case .right: position.x += offset 150 | case .up: position.z -= offset 151 | case .down: position.z += offset 152 | } 153 | } 154 | 155 | } 156 | } 157 | #endif 158 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/InfoView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | import CircuitNetwork 4 | 5 | public struct InfoView: View { 6 | @EnvironmentObject var envi: CircuitEnvironment 7 | private var model: Model? { 8 | guard let network = envi.network, let loc = envi.selectedSlot else { return nil } 9 | guard let index = envi.getModelIndexAt(location: loc) else { return nil } 10 | return network.models[index] 11 | } 12 | 13 | private var node: Node? { 14 | guard let network = envi.network, let loc = envi.selectedSlot else { return nil } 15 | guard let index = envi.getNodeIndexAt(location: loc) else { return nil } 16 | return network.nodes[index] 17 | } 18 | 19 | private var show: Bool { node != nil || model != nil} 20 | 21 | public var body: some View { 22 | Group { 23 | if show { 24 | if let node = node { 25 | NodeInfoView(node: node) 26 | .transition(.opacity) 27 | } else if let model = model { 28 | ModelInfoView(model: model) 29 | .transition(.opacity) 30 | } 31 | } 32 | }.animation(.linear, value: show) 33 | } 34 | } 35 | 36 | private struct NodeInfoView: View { 37 | @EnvironmentObject var envi: CircuitEnvironment 38 | var node: Node 39 | var name: String { node.description } 40 | var voltage: Volts { Volts(node.voltage) } 41 | public init(node: Node) { 42 | self.node = node 43 | } 44 | var body: some View { 45 | GeometryReader { geo in 46 | VStack(alignment: .custom) { 47 | HStack{ 48 | Text("Node") 49 | .font(.caption) 50 | Text(name) 51 | .font(.system(size: 32, weight: .heavy, design: .monospaced)) 52 | .alignmentGuide(.custom) { $0[.leading] } 53 | } 54 | HStack{ 55 | Text("Voltage") 56 | .font(.caption) 57 | Text(voltage.description) 58 | .font(.system(size: 16, weight: .regular, design: .monospaced)) 59 | .alignmentGuide(.custom) { $0[.leading] } 60 | } 61 | 62 | } 63 | .padding(16) 64 | .background(BlurView()) 65 | .background(Color.gray.opacity(0.25)) 66 | .cornerRadius(16) 67 | .position(x: geo.frame(in: .local).maxX - 120, y: geo.frame(in: .local).midY) 68 | } 69 | } 70 | } 71 | 72 | private struct ModelInfoView: View { 73 | var model: Model 74 | var name: String { model.description } 75 | var voltage: Volts { Volts(model.voltage) } 76 | var current: Amperes { Amperes(model.current) } 77 | var power: Watts { Watts(model.power) } 78 | 79 | public init(model: Model) { 80 | self.model = model 81 | } 82 | 83 | var body: some View { 84 | GeometryReader { geo in 85 | VStack(alignment: .custom) { 86 | HStack{ 87 | Text("Component") 88 | .font(.caption) 89 | Text(name) 90 | .font(.system(size: 32, weight: .heavy, design: .monospaced)) 91 | .alignmentGuide(.custom) { $0[.leading] } 92 | } 93 | HStack{ 94 | Text("Voltage") 95 | .font(.caption) 96 | Text(voltage.description) 97 | .font(.system(size: 16, weight: .regular, design: .monospaced)) 98 | .alignmentGuide(.custom) { $0[.leading] } 99 | } 100 | HStack{ 101 | Text("Current") 102 | .font(.caption) 103 | Text(current.description) 104 | .font(.system(size: 16, weight: .regular, design: .monospaced)) 105 | .alignmentGuide(.custom) { $0[.leading] } 106 | } 107 | HStack{ 108 | Text("Power") 109 | .font(.caption) 110 | Text(power.description) 111 | .font(.system(size: 16, weight: .regular, design: .monospaced)) 112 | .alignmentGuide(.custom) { $0[.leading] } 113 | } 114 | } 115 | .padding(16) 116 | .background(BlurView()) 117 | .background(Color.gray.opacity(0.25)) 118 | .cornerRadius(16) 119 | .position(x: geo.frame(in: .local).maxX - 120, y: geo.frame(in: .local).midY) 120 | } 121 | } 122 | } 123 | 124 | struct CustomAlignment: AlignmentID { 125 | static func defaultValue(in context: ViewDimensions) -> CGFloat { 126 | return context[.leading] 127 | } 128 | } 129 | 130 | extension HorizontalAlignment { 131 | static let custom: HorizontalAlignment = HorizontalAlignment(CustomAlignment.self) 132 | } 133 | 134 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/WireView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct WireView: View { 5 | @Environment(\.accessibilityReduceMotion) var reduceMotion 6 | 7 | private var wire: Wire 8 | 9 | private var rotationCount: Double { wire.rotationCount } 10 | 11 | public init(wire: Wire) { 12 | self.wire = wire 13 | } 14 | 15 | public var body: some View { 16 | Group { 17 | switch wire.connectionType { 18 | case .straight: 19 | WireStraightShape(wire) 20 | case .corner: 21 | WireCornerShape(wire) 22 | case .tshape: 23 | WireTshapeShape(wire) 24 | case .cross: 25 | WireCrossShape(wire) 26 | default: 27 | EmptyView() 28 | } 29 | }.mask(LinearGradient(gradient: Gradient(colors: [Color.black, Color.white]), startPoint: .leading, endPoint: .trailing)) 30 | } 31 | } 32 | 33 | public struct WireShape: View { 34 | let mask: Mask 35 | let overlay: Overlay 36 | 37 | public init(@ViewBuilder mask: () -> Mask, @ViewBuilder overlay: () -> Overlay) { 38 | self.mask = mask() 39 | self.overlay = overlay() 40 | } 41 | 42 | public var body: some View { 43 | mask 44 | .overlay( 45 | overlay 46 | .mask( 47 | mask 48 | ) 49 | ) 50 | .clipped() 51 | } 52 | } 53 | 54 | struct WireStraightShape: View { 55 | private var wire: Wire 56 | private var rotationCount: Double { wire.rotationCount } 57 | private var rotatingRemainder: Double { rotationCount.truncatingRemainder(dividingBy: 2.0) } 58 | 59 | init(_ wire: Wire) { 60 | self.wire = wire 61 | } 62 | 63 | var body: some View { 64 | WireShape(mask: { 65 | SquareImageView("WireStraightShape") 66 | .rotation3DEffect( Angle(degrees: rotationCount * 90), 67 | axis: (x: 0, y: 0, z: 1)) 68 | .scaleEffect(1.5) 69 | }, overlay: { 70 | switch rotatingRemainder { 71 | case 0: 72 | SquareImageView("WireStraight") 73 | default: 74 | SquareImageView("WireStraight") 75 | .rotation3DEffect( Angle(degrees: 90), 76 | axis: (x: 0, y: 0, z: 1)) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | struct WireCornerShape: View { 83 | private var wire: Wire 84 | private var rotationCount: Double { wire.rotationCount } 85 | private var rotatingRemainder: Double { rotationCount.truncatingRemainder(dividingBy: 4.0) } 86 | 87 | init(_ wire: Wire) { 88 | self.wire = wire 89 | } 90 | 91 | var body: some View { 92 | WireShape(mask: { 93 | SquareImageView("WireCornerShape") 94 | .rotation3DEffect( Angle(degrees: rotationCount * 90), 95 | axis: (x: 0, y: 0, z: 1)) 96 | .scaleEffect(1.5) 97 | }, overlay: { 98 | switch rotatingRemainder { 99 | case 1, -3: 100 | SquareImageView("WireCornerUR") 101 | case 2, -2: 102 | SquareImageView("WireCornerDR") 103 | case 3, -1: 104 | SquareImageView("WireCornerDL") 105 | default: 106 | SquareImageView("WireCornerUL") 107 | } 108 | }) 109 | } 110 | } 111 | 112 | struct WireTshapeShape: View { 113 | private var wire: Wire 114 | private var rotationCount: Double { wire.rotationCount } 115 | private var rotatingRemainder: Double { rotationCount.truncatingRemainder(dividingBy: 4.0) } 116 | 117 | init(_ wire: Wire) { 118 | self.wire = wire 119 | } 120 | 121 | var body: some View { 122 | WireShape(mask: { 123 | SquareImageView("WireTshapeShape") 124 | .rotation3DEffect( Angle(degrees: rotationCount * 90), 125 | axis: (x: 0, y: 0, z: 1)) 126 | .scaleEffect(1.5) 127 | }, overlay: { 128 | switch rotatingRemainder { 129 | case 1, -3: 130 | SquareImageView("WireTshapeULR") 131 | case 2, -2: 132 | SquareImageView("WireTshapeDUR") 133 | case 3, -1: 134 | SquareImageView("WireTshapeDLR") 135 | default: 136 | SquareImageView("WireTshapeDUL") 137 | } 138 | }) 139 | } 140 | } 141 | 142 | struct WireCrossShape: View { 143 | private var wire: Wire 144 | 145 | init(_ wire: Wire) { 146 | self.wire = wire 147 | } 148 | 149 | var body: some View { 150 | WireShape(mask: { 151 | SquareImageView("WireCrossShape") 152 | .scaleEffect(1.5) 153 | .rotationEffect(.degrees(90 * wire.rotationCount)) 154 | }, overlay: { 155 | SquareImageView("WireCross") 156 | }) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircuitPlay 2 | ![Header](https://github.com/devjoseadolfo/CircuitPlay/blob/main/Screenshots/Header.PNG) 3 | ## Introduction 4 | Hello, welcome to Jose Adolfo Talactac's submission for WWDC21 Swift Student Challenge, CircuitPlay! CircuitPlay is an interactive sandbox experience that allows you to design, setup, and simulate circuit. CircuitPlay is written in Swift and built with SwiftUI, Accelerate, ARKit, and RealityKit. The showcase video can be found [here](https://youtu.be/pm3mlDZJSes). 5 | 6 | 7 | ## User Interface 8 | Existing circuit simulators on the market today have cluttered and outdated user interface. For students who are new to circuit theory, they may find these simulators daunting and overwhelming. CircuitPlay aims to fill the need for a simple and intuitive circuit simulator. CircuitPlay has a user interface with touch input in mind. The fluidity of its UI is thanks to versatility and power that SwiftUI provides. 9 | 10 | CircuitPlay has three stages: Design, Setup, and Solve. In the Design stage, the user is presented with a grid and dock. The user can pick any component from the dock and drop it to a slot in the grid. Tapping a slot will show controls to rotate or delete the component. The Setup stage allows the user to change a component’s value by showing a popover prompt when the measurement is tapped. 11 | 12 | ## Circuit Solving Algorithm 13 | In the Solve stage, CircuitPlay uses its circuit solving algorithm. CircuitPlay first determines the nodes present in the circuit through recursion and matches the appropriate model for each circuit component. These nodes and models are the main parts of the circuit network. 14 | 15 | Afterwards, CircuitPlay uses the modified nodal analysis (MNA) method, where the *A* matrix and *b* vector are formed. The *A* matrix describes the conductance between each node and the connections between voltage sources and each node. The *b* vector includes the known values, which are the current across nodes and the voltage sources values. Solving the *x* vector in `Ax = b` will provide the node voltages and the current across voltage sources. 16 | 17 | The *A* matrix is sparse, meaning, there are zeroes that are present in the matrix. To leverage this, CircuitPlay utilizes Accelerate’s Sparse Solver framework to create a sparse matrix, and to factor and solve it. This is opposed to LAPACK which requires matrices to be dense. CircuitPlay only contains passive and time-domain components, thus, the matrix generated by CircuitPlay is also always symmetric. Memory use is reduced by only storing the upper triangle of the symmetric matrix. The appropriate factorization method for a symmetric and positive-indefinite matrix is the LDLᵀ decomposition. A direct method is utilized, meaning the matrix is only factored and solved once. Since there are no nonlinear components, such as diodes and transistors, it removes the need for an iterative method as there is no need to determine convergence with an I-V curve. 18 | 19 | Using the SparseSolve methods, the node voltages and voltage source currents are solved. Using Current Divider Rule and Ohm’s Law, the voltage and current across different components are determined. For time-domain components like capacitors and inductors, a linearized companion model are utilized and backwards Euler’s method is performed to determine the equivalent values of the companion model at time step *n+1*. 20 | 21 | ## Augmented Reality 22 | CircuitPlay also provides an option to view the circuit in augmented reality. This AR portion is powered by ARKit and RealityKit. RealityKit allowed CircuitPlay to display 3D models with such high performance. It uses Focus Entity, an open-source Swift Package, to easily communicate surface detection to the user. Just like Jose Adolfo's WWDC2020 submission [LearnWithAR](https://github.com/devjoseadolfo/LearnWithAR), CircuitPlay further proves that augmented reality will have a significant role in the STEM education space in the future. 23 | 24 | ## Credits 25 | 26 | - [Barlow font](https://github.com/jpt/barlow) designed by Jeremy Tribby found in the Playground thumbnail and the images in each PlaygroundPage is used under the [Open Font License](https://github.com/jpt/barlow/blob/main/OFL.txt). 27 | - San Francisco font and SF Symbols icons found in the Playground’s interface is accessed as part of iPadOS’ system fonts and used fairly within the license set by Apple Inc. for the development of software for Apple platforms. 28 | - [Focus Entity](https://github.com/maxxfrazer/FocusEntity) Swift Package by Max Cobb is used in the augmented reality component of the Playground for placing the model entity in the view. The package is used under the [MIT license](https://github.com/maxxfrazer/FocusEntity/blob/main/LICENSE). 29 | - All assets that aren’t attributed to another party are made by the creator of this Playground, Jose Adolfo Talactac. These include the instructional text and images, 2D UI icons, and 3D USDZ models. 30 | 31 | ## Screenshots 32 | ![Screenshot 1](https://github.com/devjoseadolfo/CircuitPlay/blob/main/Screenshots/Screenshot-1.PNG) 33 | ![Screenshot 2](https://github.com/devjoseadolfo/CircuitPlay/blob/main/Screenshots/Screenshot-2.PNG) 34 | ![Screenshot 3](https://github.com/devjoseadolfo/CircuitPlay/blob/main/Screenshots/Screenshot-3.PNG) 35 | ![Screenshot 4](https://github.com/devjoseadolfo/CircuitPlay/blob/main/Screenshots/Screenshot-4.PNG) 36 | ![Screenshot 5](https://github.com/devjoseadolfo/CircuitPlay/blob/main/Screenshots/Screenshot-5.PNG) 37 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/GridView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | import Combine 4 | public struct GridView: View { 5 | @EnvironmentObject var envi: CircuitEnvironment 6 | 7 | private var slots: [Slot] { 8 | envi.circuit.circuitGrid.rows.flatMap({$0.slots}) 9 | } 10 | 11 | private var columns: [GridItem] { 12 | Array(repeating: .init(.flexible(), spacing: envi.currentState == .design ? 0 : 0), count: envi.circuit.columns) 13 | } 14 | 15 | public init() {} 16 | 17 | public var body: some View { 18 | ScrollView([.horizontal, .vertical], showsIndicators: false) { 19 | ZStack{ 20 | Group { 21 | if envi.circuit.rows <= 10 { 22 | AddButton(to: .down) 23 | .offset(y: CGFloat(64 * envi.circuit.rows) + 24) 24 | AddButton(to: .up) 25 | .offset(y: CGFloat(-64 * envi.circuit.rows) - 24) 26 | } 27 | if envi.circuit.columns <= 10 { 28 | AddButton(to: .right) 29 | .offset(x: CGFloat(64 * envi.circuit.columns) + 24) 30 | AddButton(to: .left) 31 | .offset(x: CGFloat(-64 * envi.circuit.columns) - 24) 32 | } 33 | } 34 | .opacity(envi.currentState == .design ? 1 : 0) 35 | .allowsHitTesting(envi.currentState == .design) 36 | GridLines(columns: envi.circuit.columns, rows: envi.circuit.rows) 37 | .stroke(Color.secondary,lineWidth: 1.5) 38 | .opacity(envi.currentState == .setup ? 1 : 0) 39 | .blur(radius: envi.currentState == .setup ? 0 : 1) 40 | .frame(width: CGFloat(envi.circuit.columns) * 128, height: CGFloat(envi.circuit.rows) * 128) 41 | .background( 42 | GeometryReader { geo in 43 | var point = geo.frame(in: .global).origin 44 | Color.clear.preference(key: GridOriginKey.self, 45 | value: CGPoint(x: round(point.x),y: round(point.y))) 46 | .onAppear{envi.gridOrigin = geo.frame(in: .global).origin} 47 | } 48 | ) 49 | 50 | LazyVGrid(columns: columns, spacing: envi.currentState == .design ? 16 : 0) { 51 | ForEach(slots) { slot in 52 | GridSlotView(slot: $envi.circuit.circuitGrid.rows[slot.gridIndex.y].slots[slot.gridIndex.x]) 53 | .frame(width: envi.currentState == .design ? 112 : 128, height: envi.currentState == .design ? 112 : 128, alignment: .center) 54 | .simultaneousGesture(envi.currentState == .design ? drag : nil) 55 | .allowsHitTesting(envi.dragPosition == nil) 56 | .zIndex(Double(slot.zIndex)) 57 | } 58 | } 59 | } 60 | .animation(.easeOut) 61 | .padding(128) 62 | } 63 | .frame(width: .infinity, height: .infinity, alignment: .center) 64 | .background(Color(UIColor.secondarySystemBackground)) 65 | .onPreferenceChange(GridOriginKey.self) {value in 66 | DispatchQueue.main.async { 67 | envi.detector.send(value) 68 | } 69 | } 70 | .onReceive(envi.publisher, perform: { envi.gridOrigin = $0}) 71 | } 72 | 73 | var drag: some Gesture { 74 | DragGesture(minimumDistance: 1, coordinateSpace: .global) 75 | .onChanged { value in 76 | envi.firstDragIndex = envi.pointToGridIndex(point: value.startLocation, in: envi.gridRect) 77 | envi.selectedSlot = nil 78 | envi.dragPosition = envi.popoverShown ? nil : value.location 79 | 80 | } 81 | .onEnded { finalValue in 82 | let finalDragIndex = envi.pointToGridIndex(point: finalValue.location, in: envi.gridRect) 83 | envi.dragPosition = nil 84 | guard let firstIndex = envi.firstDragIndex else { return } 85 | envi.firstDragIndex = nil 86 | guard let finalIndex = finalDragIndex else { return } 87 | let firstComp = envi.circuit.circuitGrid[firstIndex].component 88 | envi.circuit.circuitGrid[firstIndex].changeComponent(to: BlankSpace()) 89 | envi.circuit.circuitGrid[finalIndex].changeComponent(to: firstComp) 90 | } 91 | } 92 | 93 | struct GridOriginKey: PreferenceKey { 94 | typealias Value = CGPoint 95 | static var defaultValue = CGPoint.zero 96 | static func reduce(value: inout Value, nextValue: () -> Value) { 97 | value.x += nextValue().x 98 | value.y += nextValue().y 99 | } 100 | } 101 | } 102 | 103 | extension View { 104 | func execute(_ closure: () -> Void) -> Self { 105 | closure() 106 | return self 107 | } 108 | } 109 | 110 | 111 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/FocusEntity.playgroundmodule/Sources/FocusEntity+Alignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusEntity.swift 3 | // FocusEntity 4 | // 5 | // Created by Max Cobb on 8/26/19. 6 | // Copyright © 2019 Max Cobb. All rights reserved. 7 | // 8 | 9 | #if canImport(ARKit) 10 | import RealityKit 11 | import ARKit 12 | import Combine 13 | 14 | extension FocusEntity { 15 | 16 | // MARK: Helper Methods 17 | 18 | /// Update the position of the focus square. 19 | internal func updatePosition() { 20 | // Average using several most recent positions. 21 | recentFocusEntityPositions = Array(recentFocusEntityPositions.suffix(10)) 22 | 23 | // Move to average of recent positions to avoid jitter. 24 | let average = recentFocusEntityPositions.reduce( 25 | SIMD3.zero, { $0 + $1 } 26 | ) / Float(recentFocusEntityPositions.count) 27 | self.position = average 28 | } 29 | 30 | /// Update the transform of the focus square to be aligned with the camera. 31 | internal func updateTransform(raycastResult: ARRaycastResult) { 32 | self.updatePosition() 33 | 34 | if state != .initializing { 35 | updateAlignment(for: raycastResult) 36 | } 37 | } 38 | 39 | internal func updateAlignment(for raycastResult: ARRaycastResult) { 40 | 41 | var targetAlignment = raycastResult.worldTransform.orientation 42 | 43 | // Determine current alignment 44 | var alignment: ARPlaneAnchor.Alignment? 45 | if let planeAnchor = raycastResult.anchor as? ARPlaneAnchor { 46 | alignment = planeAnchor.alignment 47 | // Catching case when looking at ceiling 48 | if targetAlignment.act([0, 1, 0]).y < -0.9 { 49 | targetAlignment *= simd_quatf(angle: .pi, axis: [0, 1, 0]) 50 | } 51 | } else if raycastResult.targetAlignment == .horizontal { 52 | alignment = .horizontal 53 | } else if raycastResult.targetAlignment == .vertical { 54 | alignment = .vertical 55 | } 56 | 57 | // add to list of recent alignments 58 | if alignment != nil { 59 | self.recentFocusEntityAlignments.append(alignment!) 60 | } 61 | 62 | // Average using several most recent alignments. 63 | self.recentFocusEntityAlignments = Array(self.recentFocusEntityAlignments.suffix(20)) 64 | 65 | let alignCount = self.recentFocusEntityAlignments.count 66 | let horizontalHistory = recentFocusEntityAlignments.filter({ $0 == .horizontal }).count 67 | let verticalHistory = recentFocusEntityAlignments.filter({ $0 == .vertical }).count 68 | 69 | // Alignment is same as most of the history - change it 70 | if alignment == .horizontal && horizontalHistory > alignCount * 3/4 || 71 | alignment == .vertical && verticalHistory > alignCount / 2 || 72 | raycastResult.anchor is ARPlaneAnchor { 73 | if alignment != self.currentAlignment || 74 | (alignment == .vertical && self.shouldContinueAlignAnim(to: targetAlignment) 75 | ) { 76 | isChangingAlignment = true 77 | self.currentAlignment = alignment 78 | } 79 | } else { 80 | // Alignment is different than most of the history - ignore it 81 | return 82 | } 83 | 84 | // Change the focus entity's alignment 85 | if isChangingAlignment { 86 | // Uses interpolation. 87 | // Needs to be called on every frame that the animation is desired, Not just the first frame. 88 | performAlignmentAnimation(to: targetAlignment) 89 | } else { 90 | orientation = targetAlignment 91 | } 92 | } 93 | 94 | internal func normalize(_ angle: Float, forMinimalRotationTo ref: Float) -> Float { 95 | // Normalize angle in steps of 90 degrees such that the rotation to the other angle is minimal 96 | var normalized = angle 97 | while abs(normalized - ref) > .pi / 4 { 98 | if angle > ref { 99 | normalized -= .pi / 2 100 | } else { 101 | normalized += .pi / 2 102 | } 103 | } 104 | return normalized 105 | } 106 | 107 | internal func getCamVector() -> (position: SIMD3, direciton: SIMD3)? { 108 | guard let camTransform = self.arView?.cameraTransform else { 109 | return nil 110 | } 111 | let camDirection = camTransform.matrix.columns.2 112 | return (camTransform.translation, -[camDirection.x, camDirection.y, camDirection.z]) 113 | } 114 | 115 | /// - Parameters: 116 | /// - Returns: ARRaycastResult if an existing plane geometry or an estimated plane are found, otherwise nil. 117 | internal func smartRaycast() -> ARRaycastResult? { 118 | // Perform the hit test. 119 | guard let (camPos, camDir) = self.getCamVector() else { 120 | return nil 121 | } 122 | let rcQuery = ARRaycastQuery( 123 | origin: camPos, direction: camDir, 124 | allowing: self.allowedRaycast, alignment: .any 125 | ) 126 | let results = self.arView?.session.raycast(rcQuery) ?? [] 127 | 128 | // 1. Check for a result on an existing plane using geometry. 129 | if let existingPlaneUsingGeometryResult = results.first( 130 | where: { $0.target == .existingPlaneGeometry } 131 | ) { 132 | return existingPlaneUsingGeometryResult 133 | } 134 | 135 | // 2. As a fallback, check for a result on estimated planes. 136 | return results.first(where: { $0.target == .estimatedPlane }) 137 | } 138 | 139 | /// Uses interpolation between orientations to create a smooth `easeOut` orientation adjustment animation. 140 | internal func performAlignmentAnimation(to newOrientation: simd_quatf) { 141 | // Interpolate between current and target orientations. 142 | orientation = simd_slerp(orientation, newOrientation, 0.15) 143 | // This length creates a normalized vector (of length 1) with all 3 components being equal. 144 | self.isChangingAlignment = self.shouldContinueAlignAnim(to: newOrientation) 145 | } 146 | 147 | func shouldContinueAlignAnim(to newOrientation: simd_quatf) -> Bool { 148 | let testVector = simd_float3(repeating: 1 / sqrtf(3)) 149 | let point1 = orientation.act(testVector) 150 | let point2 = newOrientation.act(testVector) 151 | let vectorsDot = simd_dot(point1, point2) 152 | // Stop interpolating when the rotations are close enough to each other. 153 | return vectorsDot < 0.999 154 | } 155 | 156 | /** 157 | Reduce visual size change with distance by scaling up when close and down when far away. 158 | 159 | These adjustments result in a scale of 1.0x for a distance of 0.7 m or less 160 | (estimated distance when looking at a table), and a scale of 1.2x 161 | for a distance 1.5 m distance (estimated distance when looking at the floor). 162 | */ 163 | internal func scaleBasedOnDistance(camera: ARCamera?) -> Float { 164 | guard let camera = camera else { return 1.0 } 165 | 166 | let distanceFromCamera = simd_length(self.convert(position: .zero, to: nil) - camera.transform.translation) 167 | if distanceFromCamera < 0.7 { 168 | return distanceFromCamera / 0.7 169 | } else { 170 | return 0.25 * distanceFromCamera + 0.825 171 | } 172 | } 173 | } 174 | #endif 175 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/SlotView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct GridSlotView: View { 5 | @EnvironmentObject var envi: CircuitEnvironment 6 | @Binding private var slot: Slot 7 | @Environment (\.colorScheme) private var colorScheme: ColorScheme 8 | @State private var modelIndex: Int? 9 | @State private var nodeIndex: Int? 10 | private var showDesignSideButton: Bool { slot.selected && envi.currentState == .design } 11 | 12 | public init(slot: Binding) { 13 | self._slot = slot 14 | } 15 | 16 | var isVertical: Bool { 17 | guard slot.component is Connectable else { return false } 18 | let component = slot.component as! Connectable 19 | if slot.component is Capacitor { 20 | return component.contacts.orientation != .vertical 21 | } 22 | return component.contacts.orientation == .vertical 23 | } 24 | 25 | public var body: some View { 26 | ZStack { 27 | Group { 28 | background 29 | main 30 | stroke 31 | }.allowsHitTesting(envi.currentState != .setup) 32 | if envi.currentState == .play { 33 | if let index = modelIndex, 34 | let network = envi.network, 35 | let model = network.models[index], 36 | let direction = model.currentGoing, 37 | model.current != 0 { 38 | Group { 39 | CurrentSignView(direction: direction) 40 | if !(slot.component is Battery || slot.component is CurrentSource) { 41 | SignsView(positiveDirection: direction.opposite) 42 | } 43 | } 44 | .shadow(radius: envi.currentState == .design ? 0 : 2.5, 45 | x: envi.currentState == .design ? 0 : -2.5, 46 | y: envi.currentState == .design ? 0 : 2.5) 47 | GeometryReader { geo in 48 | NameView(name: model.description) 49 | .rotationEffect(isVertical ? .degrees(-90) : .zero) 50 | .position(x: isVertical ? 1 * geo.frame(in: .local).width / 4 : geo.frame(in: .local).midX, 51 | y: isVertical ? geo.frame(in: .local).midY : 1 * geo.frame(in: .local).height / 4) 52 | .offset(x: isVertical && slot.component.componentType.imageName == "Source" ? -geo.frame(in: .local).width / 8 : 0, y: !isVertical && slot.component.componentType.imageName == "Source" ? -geo.frame(in: .local).height / 8 : 0) 53 | } 54 | } 55 | } 56 | if envi.currentState != .design { 57 | measurement 58 | } 59 | } 60 | .frame(alignment: .center) 61 | .overlay(sideOverlay) 62 | .offset(x: slot.selected && envi.currentState == .design ? 8 : 0, 63 | y: slot.selected && envi.currentState == .design ? -8 : 0) 64 | .onTapGesture(count: 1) { 65 | slot.selected = !slot.selected 66 | envi.selectedSlot = slot.selected ? slot.gridIndex : nil 67 | } 68 | .allowsHitTesting(slot.component.componentType != .spacer) 69 | .onChange(of: envi.currentState) { 70 | if $0 == .play { 71 | modelIndex = envi.getModelIndexAt(location: slot.gridIndex) 72 | nodeIndex = envi.getNodeIndexAt(location: slot.gridIndex) 73 | } 74 | } 75 | } 76 | 77 | var main: some View { 78 | ComponentView(component: slot.component) 79 | .transition(.fadeAndScale) 80 | .shadow(radius: envi.currentState == .design ? 0 : 2.5, 81 | x: envi.currentState == .design ? 0 : -2.5, 82 | y: envi.currentState == .design ? 0 : 2.5) 83 | } 84 | 85 | var stroke: some View { 86 | RoundedRectangle(cornerRadius: envi.currentState == .design ? 8 : 0) 87 | .stroke(slot.gridIndex == envi.dragGridIndex ?? Location(-1,-1) ? Color.green : Color.clear.getBackground(for: colorScheme), 88 | lineWidth: envi.currentState == .design ? 4 : 0) 89 | 90 | } 91 | 92 | var background: some View { 93 | GeometryReader { geo in 94 | RoundedRectangle(cornerRadius: envi.currentState == .setup ? 0 : 8) 95 | .fill(slot.selected ? Color(red: 0.353, green: 0.521, blue: 0.652, opacity: 1.000) : Color.clear) 96 | .shadow( 97 | radius: slot.selected && envi.currentState == .design ? 10 : 0, 98 | x: slot.selected && envi.currentState == .design ? -5 : 0, 99 | y: slot.selected && envi.currentState == .design ? 5 : 0) 100 | .frame(width: geo.size.width - 1, height: geo.size.height - 1, alignment: .center) 101 | .offset(x: 0.5, y: 0.5) 102 | } 103 | } 104 | var sideOverlay: some View { 105 | DesignSideButtonsView() 106 | .offset(x: showDesignSideButton ? 88: 60) 107 | .opacity(showDesignSideButton ? 1 : 0) 108 | .allowsHitTesting(showDesignSideButton) 109 | } 110 | 111 | var measurement: some View { 112 | GeometryReader { geo in 113 | if !slot.component.measurements.isEmpty { 114 | MeasurementView(measurement: $slot.component.measurements[0]) 115 | .rotationEffect(isVertical ? .degrees(-90) : .zero) 116 | .position(x: isVertical ? 3 * geo.frame(in: .local).width / 4 : geo.frame(in: .local).midX, 117 | y: isVertical ? geo.frame(in: .local).midY : 3 * geo.frame(in: .local).height / 4) 118 | .offset(x: isVertical && slot.component.componentType.imageName == "Source" ? geo.frame(in: .local).width / 8 : 0, y: !isVertical && slot.component.componentType.imageName == "Source" ? geo.frame(in: .local).height / 8 : 0) 119 | 120 | } 121 | } 122 | } 123 | 124 | var name: some View { 125 | GeometryReader { geo in 126 | if !slot.component.measurements.isEmpty { 127 | MeasurementView(measurement: $slot.component.measurements[0]) 128 | .rotationEffect(isVertical ? .degrees(-90) : .zero) 129 | .position(x: isVertical ? 3 * geo.frame(in: .local).width / 4 : geo.frame(in: .local).midX, 130 | y: isVertical ? geo.frame(in: .local).midY : 3 * geo.frame(in: .local).height / 4) 131 | .offset(x: isVertical && slot.component.componentType.imageName == "Source" ? geo.frame(in: .local).width / 8 : 0, y: !isVertical && slot.component.componentType.imageName == "Source" ? geo.frame(in: .local).height / 8 : 0) 132 | 133 | } 134 | } 135 | } 136 | } 137 | 138 | extension AnyTransition { 139 | static var fadeAndScale: AnyTransition { 140 | AnyTransition.opacity.combined(with: .scale) 141 | } 142 | } 143 | 144 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/Network.swift: -------------------------------------------------------------------------------- 1 | import CircuitKit 2 | import Accelerate 3 | 4 | public struct CircuitNetwork { 5 | public var circuit: Circuit 6 | 7 | public var timeStep: Double = 0.01 8 | public var step: Int = 0 9 | 10 | public var nodes: [Node] = [Node(0)] 11 | public var models: [Model] = [] 12 | 13 | 14 | public var conductanceMatrix: SparseMatrix_Double? 15 | public var values: [Double] = [] 16 | public var invertedConductanceMatrix: SparseMatrix_Double? 17 | public var xVector: [Double] = [] 18 | public var bVector: [Double] = [] 19 | 20 | public var groundCount: Int = 0 21 | 22 | public init(circuit: Circuit) { 23 | self.circuit = circuit 24 | // print("Making ground node") 25 | makeGroundNode() 26 | // print("Making rest of the network") 27 | makeNodesAndModels() 28 | // print("Assigning connections") 29 | assignConnections() 30 | makeHiddenNodes() 31 | // attachHiddenGroundNodes() 32 | assignInductorNodes() 33 | } 34 | 35 | public mutating func solveNetwork() { 36 | createConductanceMatrix() 37 | createBVector() 38 | createXVector() 39 | solveSparseMatrix() 40 | // print("b", bVector) 41 | // print("x", xVector) 42 | // multiplyInverseMatrixToVector() 43 | updateNodeVoltages() 44 | updateModelVoltage() 45 | updateModelCurrent() 46 | } 47 | 48 | public mutating func updateNodeVoltages() { 49 | for i in 1.. 0 else { 58 | continue 59 | } 60 | let firstNode = modelNodes[0] 61 | let secondNode = modelNodes[1] 62 | let firstVoltage = nodes[firstNode.1].voltage 63 | let secondVoltage = nodes[secondNode.1].voltage 64 | let voltageDifference = firstVoltage - secondVoltage 65 | models[i].voltage = abs(voltageDifference) 66 | 67 | if models[i] is CapacitorModel { 68 | var capacitor = models[i] as! CapacitorModel 69 | if voltageDifference > 0 { 70 | capacitor.positiveTerminalNode = firstNode.1 71 | capacitor.negativeTerminalNode = secondNode.1 72 | capacitor.currentGoing = secondNode.0 73 | } else if voltageDifference < 0 { 74 | capacitor.positiveTerminalNode = secondNode.1 75 | capacitor.negativeTerminalNode = firstNode.1 76 | capacitor.currentGoing = firstNode.0 77 | } else { 78 | capacitor.positiveTerminalNode = nil 79 | capacitor.negativeTerminalNode = nil 80 | capacitor.currentGoing = nil 81 | } 82 | models[i] = capacitor 83 | } else if models[i] is InductorModel { 84 | var inductor = models[i] as! InductorModel 85 | if voltageDifference > 0 { 86 | inductor.positiveTerminalNode = firstNode.1 87 | inductor.negativeTerminalNode = secondNode.1 88 | inductor.currentGoing = secondNode.0 89 | } else if voltageDifference < 0 { 90 | inductor.positiveTerminalNode = secondNode.1 91 | inductor.negativeTerminalNode = firstNode.1 92 | inductor.currentGoing = firstNode.0 93 | } 94 | models[i] = inductor 95 | } else if !(models[i] is VoltageSourceModel || models[i] is CurrentSourceModel) { 96 | if voltageDifference > 0 { 97 | models[i].currentGoing = secondNode.0 98 | } else if voltageDifference < 0 { 99 | models[i].currentGoing = firstNode.0 100 | } else { 101 | models[i].currentGoing = nil 102 | } 103 | } 104 | } 105 | } 106 | 107 | public mutating func updateModelCurrent() { 108 | let voltageSources = models 109 | .filter{$0 is VoltageSourceModel} 110 | .map{$0 as! VoltageSourceModel} 111 | 112 | for source in voltageSources { 113 | let current = xVector[nodeCount - 1 + source.number] 114 | let sourceIndex = models.firstIndex {$0.description == source.description} 115 | models[sourceIndex!].current = abs(current) 116 | if let sourceComp = source.component as? Battery { 117 | if current > 0 { 118 | models[sourceIndex!].currentGoing = sourceComp.terminals.negative 119 | } else if current < 0 { 120 | models[sourceIndex!].currentGoing = sourceComp.terminals.positive 121 | } 122 | } 123 | } 124 | 125 | let inductors = models 126 | .filter{$0 is InductorModel} 127 | .map{$0 as! InductorModel} 128 | 129 | for inductor in inductors { 130 | let current = xVector[nodeCount + voltageSources.count + inductor.number - 1] 131 | let sourceIndex = models.firstIndex {$0.description == inductor.description} 132 | models[sourceIndex!].current = abs(current) 133 | } 134 | 135 | let remainingModels = models.filter{$0 is ResistorModel || $0 is CapacitorModel} 136 | 137 | for model in remainingModels { 138 | let matchedModels = remainingModels 139 | .filter{$0.connectedNodes.map{$0.value}.sorted() == model.connectedNodes.map{$0.value}.sorted()} 140 | 141 | var equivalentResistance: Double = 0 142 | var totalResistance: Double = 0 143 | for model in matchedModels { 144 | if let resistor = model as? ResistorModel { 145 | equivalentResistance += 1 / resistor.resistance 146 | totalResistance += resistor.resistance 147 | } 148 | if let capacitor = model as? CapacitorModel { 149 | equivalentResistance += 1 / capacitor.equivalentResistance 150 | totalResistance += capacitor.equivalentResistance 151 | } 152 | } 153 | 154 | var modelResistance: Double = 0 155 | 156 | if let resistor = model as? ResistorModel { 157 | modelResistance = resistor.resistance 158 | } 159 | if let capacitor = model as? CapacitorModel { 160 | modelResistance = capacitor.equivalentResistance 161 | } 162 | let modelIndex = models.firstIndex{$0.description == model.description} 163 | models[modelIndex!].current = abs(model.voltage * equivalentResistance * ( modelResistance / totalResistance)) 164 | } 165 | } 166 | } 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitUI.playgroundmodule/Sources/MeasurementEditView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CircuitKit 3 | 4 | public struct MeasurementEditView: View { 5 | @EnvironmentObject var envi: CircuitEnvironment 6 | @Binding private var measurement: CircuitUnit 7 | @Binding private var show: Bool 8 | @State private var value: Double = 0 9 | @State private var multiplier: Double = 1 10 | @State private var field = "" 11 | @State private var isNegative: Bool = false 12 | 13 | private var unitPrefix: UnitPrefix { 14 | return getPrefix(value: multiplier) 15 | } 16 | private var baseUnit: BaseUnit = .watt 17 | 18 | private var fieldLimit: Int = 3 19 | 20 | public init(measurement: Binding, show: Binding) { 21 | self._measurement = measurement 22 | self._show = show 23 | self.value = measurement.wrappedValue.value 24 | self.baseUnit = measurement.wrappedValue.base 25 | multiplier = pow(10, Double(measurement.wrappedValue.unitPrefix.rawValue)) 26 | self.multiplier = multiplier.isZero ? 1 : multiplier 27 | } 28 | 29 | var display: String { 30 | let sign = isNegative ? "-" : "+" 31 | return sign + field + " " + unitPrefix.symbol + baseUnit.symbol 32 | } 33 | 34 | public var body: some View { 35 | HStack{ 36 | VStack(spacing: 4) { 37 | Text(display) 38 | .font(.system(size: 13.5, weight: .regular, design: .monospaced)) 39 | .multilineTextAlignment(.leading) 40 | .frame(width: 102, height: 32) 41 | .overlay(RoundedRectangle(cornerRadius: 5) 42 | .stroke(Color.gray.opacity(0.25), lineWidth: 2) 43 | .frame(width: 102)) 44 | numPad 45 | } 46 | unitPicker 47 | }.padding(10) 48 | } 49 | 50 | private var columns: [GridItem] { 51 | Array(repeating: .init(.fixed(32), spacing: 4), count: 3) 52 | } 53 | 54 | var numPad: some View { 55 | LazyVGrid(columns: columns, spacing: 4) { 56 | ForEach(NumPadButton.allCases, id: \.self) { number in 57 | Button { 58 | switch number { 59 | case .plusminus: 60 | isNegative.toggle() 61 | default: 62 | field.append(number.rawValue) 63 | } 64 | } label: { 65 | Label(number.rawValue, systemImage: number.sfSymbol) 66 | .labelStyle(IconOnlyLabelStyle()) 67 | .font(.system(size: 24, weight: .regular)) 68 | .imageScale(.small) 69 | .scaleEffect(number == .decimal ? 0.25 : 1) 70 | } 71 | .buttonStyle(PadButtonStyle(color: Color.gray)) 72 | .opacity(getBool(for: number, in: field) ? 1 : 0.25) 73 | .allowsHitTesting(getBool(for: number, in: field)) 74 | } 75 | } 76 | } 77 | 78 | func getBool(for button: NumPadButton, in field: String) -> Bool{ 79 | switch button { 80 | case .plusminus: 81 | return !(measurement is Ohms || measurement is Farads || measurement is Henries || measurement is Hertz) 82 | case .decimal: 83 | return field.count > 0 && field.count <= fieldLimit && !field.contains(".") 84 | default: 85 | return field.count <= fieldLimit && (!field.hasSuffix("0") || field.count > 1) 86 | } 87 | } 88 | 89 | 90 | var unitPicker: some View { 91 | VStack(spacing: 4){ 92 | Button { 93 | let sign: Double = isNegative ? -1 : 1 94 | let finalValue = (Double(field) ?? 0) * multiplier * sign 95 | measurement = measurement.base.createUnit(from: finalValue) 96 | show = false 97 | } 98 | label: { 99 | Label("Done", systemImage: "checkmark") 100 | .font(Font.body.bold()) 101 | .labelStyle(IconOnlyLabelStyle()) 102 | .foregroundColor(Color.green) 103 | } 104 | .buttonStyle(PadButtonStyle(color: Color.green)) 105 | .opacity(field.count > 0 && !field.hasSuffix(".") ? 1 : 0.25) 106 | .allowsHitTesting(field.count > 0 && !field.hasSuffix(".")) 107 | Button { multiplier *= 1000 } 108 | label: { 109 | Label("Increase", systemImage: "chevron.up") 110 | .font(Font.body.bold()) 111 | .labelStyle(IconOnlyLabelStyle()) 112 | } 113 | .buttonStyle(PadButtonStyle(color: Color.blue)) 114 | .opacity(unitPrefix != .giga ? 1 : 0.25) 115 | .allowsHitTesting(unitPrefix != .giga) 116 | Text(unitPrefix.symbol + baseUnit.symbol) 117 | .font(.system(size: 13.5, weight: .regular, design: .monospaced)) 118 | .frame( height: 32) 119 | .overlay(RoundedRectangle(cornerRadius: 5) 120 | .stroke(Color.gray.opacity(0.25), lineWidth: 2) 121 | .frame(width: 31)) 122 | Button { multiplier *= 0.001 } 123 | label: { 124 | Label("Decrease", systemImage: "chevron.down") 125 | .font(Font.body.bold()) 126 | .labelStyle(IconOnlyLabelStyle()) 127 | } 128 | .buttonStyle(PadButtonStyle(color: Color.blue)) 129 | .opacity(unitPrefix != .nano ? 1 : 0.25) 130 | .allowsHitTesting(unitPrefix != .nano) 131 | Button { field = "" } 132 | label: { 133 | Label("Clear", systemImage: "c.circle") 134 | .font(Font.body.bold()) 135 | .foregroundColor(Color.red) 136 | .labelStyle(IconOnlyLabelStyle()) 137 | } 138 | .opacity(!field.isEmpty ? 1 : 0.25) 139 | .allowsHitTesting(!field.isEmpty) 140 | .buttonStyle(PadButtonStyle(color: Color.red)) 141 | } 142 | } 143 | } 144 | 145 | enum NumPadButton: String, CaseIterable, Identifiable{ 146 | case one = "1", two = "2", three = "3" 147 | case four = "4" , five = "5" , six = "6" 148 | case seven = "7", eight = "8", nine = "9" 149 | case decimal = ".", zero = "0", plusminus 150 | 151 | var sfSymbol: String { 152 | switch self { 153 | case .decimal: 154 | return "circlebadge.fill" 155 | case .plusminus: 156 | return "plusminus" 157 | default: 158 | return self.rawValue + ".circle" 159 | } 160 | } 161 | 162 | var id: String { self.rawValue } 163 | } 164 | 165 | public struct PadButtonStyle: ButtonStyle { 166 | var color: Color 167 | public init(color: Color){ 168 | self.color = color 169 | } 170 | public func makeBody(configuration: Configuration) -> some View { 171 | configuration.label 172 | .scaleEffect(configuration.isPressed ? 1.25 : 1.0) 173 | .frame(width: 24, height: 24, alignment: .center) 174 | .aspectRatio(1.0, contentMode: .fit) 175 | .padding(4) 176 | .background(configuration.isPressed ? color.opacity(0.25) : Color.gray.opacity(0.125)) 177 | .cornerRadius(4) 178 | .shadow(radius: configuration.isPressed ? 7.5 : 0) 179 | .animation(.linear(duration: 0.25), value: configuration.isPressed) 180 | } 181 | } 182 | 183 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitKit.playgroundmodule/Sources/Units.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | public protocol CircuitUnit: CustomStringConvertible { 3 | var dimension: Dimension { get } 4 | var value: Double { get set } 5 | var unitPrefix: UnitPrefix { get } 6 | var base: BaseUnit { get } 7 | var prefixValue: Double { get } 8 | } 9 | 10 | protocol AddAndSub { 11 | static func + (rhs: Self, lhs: Self) -> Self 12 | static func - (rhs: Self, lhs: Self) -> Self 13 | } 14 | 15 | public func getPrefix(value: Double) -> UnitPrefix { 16 | if value.isZero { return .base } 17 | let log: Int = Int(floor(log10(abs(value)) / 3) * 3) 18 | return UnitPrefix(rawValue: log) ?? (log < 0 ? .pico : .tera) 19 | } 20 | 21 | func getDescription(_ unit: CircuitUnit) -> String { 22 | let fmt = NumberFormatter() 23 | fmt.numberStyle = .decimal 24 | fmt.minimumSignificantDigits = 3 25 | fmt.maximumSignificantDigits = 3 26 | fmt.string(for: unit.prefixValue) 27 | let finalValue = fmt.string(for: unit.prefixValue) ?? String(format: "%.2f", unit.prefixValue) 28 | return finalValue + " " + unit.unitPrefix.symbol + unit.base.symbol 29 | } 30 | 31 | 32 | public struct Volts: CircuitUnit, AddAndSub { 33 | public var dimension: Dimension = .voltage 34 | public var value: Double 35 | public var unitPrefix: UnitPrefix { getPrefix(value: value) } 36 | public var base: BaseUnit = .volt 37 | public var prefixValue: Double { 38 | let power = pow(10, Double(unitPrefix.rawValue)) 39 | return unitPrefix == .base ? value : value / power 40 | } 41 | 42 | public init(_ value: Double = 0) { 43 | self.value = value 44 | } 45 | 46 | public var description: String { getDescription(self) } 47 | 48 | public static func + (lhs: Self, rhs: Self) -> Self { 49 | return Self(lhs.value + rhs.value) 50 | } 51 | 52 | public static func - (lhs: Self, rhs: Self) -> Self { 53 | return Self(lhs.value - rhs.value) 54 | } 55 | } 56 | 57 | public struct Ohms: CircuitUnit, AddAndSub { 58 | public var dimension: Dimension = .resistance 59 | public var value: Double 60 | public var unitPrefix: UnitPrefix { getPrefix(value: value) } 61 | public var base: BaseUnit = .ohm 62 | public var prefixValue: Double { 63 | let power = pow(10, Double(unitPrefix.rawValue)) 64 | return unitPrefix == .base ? value : value / power 65 | } 66 | 67 | public init(_ value: Double = 0) { 68 | self.value = value 69 | } 70 | 71 | public var description: String { getDescription(self) } 72 | 73 | public static func + (lhs: Self, rhs: Self) -> Self { 74 | return Self(lhs.value + rhs.value) 75 | } 76 | 77 | public static func - (lhs: Self, rhs: Self) -> Self { 78 | return Self(lhs.value - rhs.value) 79 | } 80 | } 81 | 82 | public struct Amperes: CircuitUnit, AddAndSub { 83 | public var dimension: Dimension = .current 84 | public var value: Double 85 | public var unitPrefix: UnitPrefix { getPrefix(value: value) } 86 | public var base: BaseUnit = .ampere 87 | public var prefixValue: Double { 88 | let power = pow(10, Double(unitPrefix.rawValue)) 89 | return unitPrefix == .base ? value : value / power 90 | } 91 | 92 | public init(_ value: Double = 0) { 93 | self.value = value 94 | } 95 | 96 | public var description: String { getDescription(self) } 97 | 98 | public static func + (lhs: Self, rhs: Self) -> Self { 99 | return Self(lhs.value + rhs.value) 100 | } 101 | 102 | public static func - (lhs: Self, rhs: Self) -> Self { 103 | return Self(lhs.value - rhs.value) 104 | } 105 | } 106 | 107 | public struct Watts: CircuitUnit, AddAndSub { 108 | public var dimension: Dimension = .power 109 | public var value: Double 110 | public var unitPrefix: UnitPrefix { getPrefix(value: value) } 111 | public var base: BaseUnit = .watt 112 | public var prefixValue: Double { 113 | let power = pow(10, Double(unitPrefix.rawValue)) 114 | return unitPrefix == .base ? value : value / power 115 | } 116 | 117 | public init(_ value: Double = 0) { 118 | self.value = value 119 | } 120 | 121 | public var description: String { getDescription(self) } 122 | 123 | public static func + (lhs: Self, rhs: Self) -> Self { 124 | return Self(lhs.value + rhs.value) 125 | } 126 | 127 | public static func - (lhs: Self, rhs: Self) -> Self { 128 | return Self(lhs.value - rhs.value) 129 | } 130 | } 131 | 132 | 133 | public struct Hertz: CircuitUnit, AddAndSub { 134 | public var dimension: Dimension = .frequency 135 | public var value: Double 136 | public var unitPrefix: UnitPrefix { getPrefix(value: value) } 137 | public var base: BaseUnit = .hertz 138 | public var prefixValue: Double { 139 | let power = pow(10, Double(unitPrefix.rawValue)) 140 | return unitPrefix == .base ? value : value / power 141 | } 142 | 143 | public init(_ value: Double = 0) { 144 | self.value = value 145 | } 146 | 147 | public var description: String { let fmt = NumberFormatter() 148 | fmt.numberStyle = .decimal 149 | fmt.maximumFractionDigits = 0 150 | fmt.string(for: prefixValue) 151 | let finalValue = fmt.string(for: prefixValue) ?? String(format: "%.2f", prefixValue) 152 | return finalValue + " " + unitPrefix.symbol + base.symbol 153 | } 154 | 155 | public static func + (lhs: Self, rhs: Self) -> Self { 156 | return Self(lhs.value + rhs.value) 157 | } 158 | 159 | public static func - (lhs: Self, rhs: Self) -> Self { 160 | return Self(lhs.value - rhs.value) 161 | } 162 | } 163 | 164 | public struct Henries: CircuitUnit, AddAndSub { 165 | public var dimension: Dimension = .inductance 166 | public var value: Double 167 | public var unitPrefix: UnitPrefix { getPrefix(value: value) } 168 | public var base: BaseUnit = .henry 169 | public var prefixValue: Double { 170 | let power = pow(10, Double(unitPrefix.rawValue)) 171 | return unitPrefix == .base ? value : value / power 172 | } 173 | 174 | public init(_ value: Double = 0) { 175 | self.value = value 176 | } 177 | 178 | public var description: String { getDescription(self) } 179 | 180 | public static func + (lhs: Self, rhs: Self) -> Self { 181 | return Self(lhs.value + rhs.value) 182 | } 183 | 184 | public static func - (lhs: Self, rhs: Self) -> Self { 185 | return Self(lhs.value - rhs.value) 186 | } 187 | } 188 | 189 | public struct Farads: CircuitUnit, AddAndSub { 190 | public var dimension: Dimension = .capacitance 191 | public var value: Double 192 | public var unitPrefix: UnitPrefix { getPrefix(value: value) } 193 | public var base: BaseUnit = .farad 194 | public var prefixValue: Double { 195 | let power = pow(10, Double(unitPrefix.rawValue)) 196 | return unitPrefix == .base ? value : value / power 197 | } 198 | 199 | public init(_ value: Double = 0) { 200 | self.value = value 201 | } 202 | 203 | public var description: String { getDescription(self) } 204 | 205 | public static func + (lhs: Self, rhs: Self) -> Self { 206 | return Self(lhs.value + rhs.value) 207 | } 208 | 209 | public static func - (lhs: Self, rhs: Self) -> Self { 210 | return Self(lhs.value - rhs.value) 211 | } 212 | } 213 | 214 | extension Double { 215 | public func volts() -> Volts { 216 | return Volts(self) 217 | } 218 | public func amperes() -> Amperes { 219 | return Amperes(self) 220 | } 221 | public func ohms() -> Ohms { 222 | return Ohms(self) 223 | } 224 | public func watts() -> Watts { 225 | return Watts(self) 226 | } 227 | public func henries() -> Henries { 228 | return Henries(self) 229 | } 230 | public func farads() -> Farads { 231 | return Farads(self) 232 | } 233 | public func hertz() -> Hertz { 234 | return Hertz(self) 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /CircuitPlay.playgroundbook/Contents/UserModules/CircuitNetwork.playgroundmodule/Sources/Network+Solver.swift: -------------------------------------------------------------------------------- 1 | import Accelerate 2 | 3 | extension CircuitNetwork { 4 | public var nodeCount: Int { nodes.count } 5 | public var voltageSourceCount: Int { models.filter{$0 is VoltageSourceModel || $0 is InductorModel}.count } 6 | 7 | public mutating func createBVector() { 8 | var values: [Double] = [] 9 | 10 | var minusGroundNode = nodes 11 | minusGroundNode.removeFirst() 12 | 13 | for node in minusGroundNode { 14 | let currentSources = models 15 | .filter{ $0 is CurrentSourceModel } 16 | .filter{ model in 17 | model.connectedNodes.contains { possNode in 18 | possNode.value == node.number 19 | } 20 | } 21 | .map { 22 | $0 as! CurrentSourceModel 23 | } 24 | 25 | let capacitors = models 26 | .filter{ $0 is CapacitorModel } 27 | .filter{ model in 28 | model.connectedNodes.contains { possNode in 29 | possNode.value == node.number 30 | } 31 | } 32 | .map { 33 | $0 as! CapacitorModel 34 | } 35 | 36 | var current: Double = 0 37 | 38 | for source in currentSources { 39 | if source.positiveTerminalNode == node.number { 40 | current += source.current 41 | } else if source.negativeTerminalNode == node.number { 42 | current -= source.current 43 | } 44 | } 45 | 46 | for capacitor in capacitors { 47 | if capacitor.positiveTerminalNode == node.number { 48 | current += capacitor.equivalentCurrent 49 | } else if capacitor.negativeTerminalNode == node.number { 50 | current -= capacitor.equivalentCurrent 51 | } 52 | } 53 | 54 | values.append(current) 55 | } 56 | 57 | let voltageSources = models 58 | .filter{ $0 is VoltageSourceModel } 59 | .map { $0 as! VoltageSourceModel} 60 | 61 | for source in voltageSources { 62 | values.append(source.voltage) 63 | } 64 | 65 | let inductors = models 66 | .filter{ $0 is InductorModel } 67 | .map { $0 as! InductorModel} 68 | 69 | for inductor in inductors { 70 | values.append(inductor.equivalentVoltage) 71 | } 72 | 73 | bVector = values 74 | } 75 | 76 | public mutating func createXVector() { 77 | xVector = Array(repeating: 0, count: nodes.count + voltageSourceCount - 1) 78 | } 79 | 80 | public mutating func createConductanceMatrix() { 81 | var nodeVectors: [[Double]] = [] 82 | 83 | var minusGroundNode = nodes 84 | minusGroundNode.removeFirst() 85 | 86 | var row: [Int32] = [Int32]() 87 | var column: [Int32] = [Int32]() 88 | var value: [Double] = [Double]() 89 | for i in 0..