1
0
mirror of https://github.com/teoxoy/factorio-blueprint-editor.git synced 2025-03-27 21:39:03 +02:00

Adds circuit wires (#248)

* add option to allow unreachable wire connections
this affects how the entities can be repositionned

* refactor IConnection to allow arbitrary wire connection points

* made getEntityAtPosition take a IPoint instead of x and y

* add util.sumprod function for vector manipulation

* add helper functions for locating wire connection points
namely:
- Entity.getWireConnectionBoundingBox
- PositionGrid.getConnectionPointAtPosition

* add circuit wire placement from the UI

* fix tile rotation (e.g., hazard concrete)

* make the connection point bounding box a full cell when it is single

* make wire "rotation" cycling from red to green

* make BlueprintContainer default settings reflect the saved settings

(dirty fix, it would have been cleaner to save the settings outside of BPC)

* mostly fixing typos

* make pressing the pipette (Q) while placing wire cancel the wire
This commit is contained in:
Cryhot 2023-02-20 15:44:09 -06:00 committed by GitHub
parent 21ab873d83
commit 0bec144b89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 529 additions and 173 deletions

View File

@ -93,6 +93,13 @@ export class Editor {
G.UI.quickbarPanel.generateSlots(items)
}
public get limitWireReach(): boolean {
return G.BPC.limitWireReach
}
public set limitWireReach(limit: boolean) {
G.BPC.limitWireReach = limit
}
public get oilOutpostSettings(): IOilOutpostSettings {
return oilOutpostSettings
}
@ -231,7 +238,7 @@ export class Editor {
: entity.direction
G.BPC.spawnPaintContainer(itemName, direction)
} else if (G.BPC.mode === EditorMode.PAINT) {
G.BPC.paintContainer.destroy()
G.BPC.paintContainer.destroy(true)
}
G.BPC.exitCopyMode(true)
G.BPC.exitDeleteMode(true)

View File

@ -232,14 +232,12 @@ export class EntityInfoPanel extends Panel {
// const fromP = util.rotatePointBasedOnDir([0, -tiles], entity.direction)
const toP = util.rotatePointBasedOnDir([0, tiles], entity.direction)
// const from = G.bp.entities.get(
// G.bp.entityPositionGrid.getCellAtPosition({
// x: entity.position.x + fromP.x,
// y: entity.position.y + fromP.y
// })
// G.bp.entityPositionGrid.getCellAtPosition(
// util.sumprod(entity.position, fromP)
// )
// )
const to = G.bp.entityPositionGrid.getEntityAtPosition(
entity.position.x + toP.x,
entity.position.y + toP.y
util.sumprod(entity.position, toP)
)
if (to && isBelt(to)) {
speed = containerToBelt(

View File

@ -65,7 +65,7 @@ export class InventoryDialog extends Dialog {
let groupIndex = 0
for (const group of FD.inventoryLayout) {
// Make creative entities avalible only in the main inventory
// Make creative entities available only in the main inventory
if (group.name === 'creative' && itemsFilter !== undefined) {
continue
}
@ -81,7 +81,7 @@ export class InventoryDialog extends Dialog {
if (itemsFilter === undefined) {
const itemData = FD.items[item.name]
if (!itemData) continue
if (!itemData.place_result && !itemData.place_as_tile) continue
if (!itemData.place_result && !itemData.place_as_tile && !itemData.wire_count) continue
// needed for robots/trains/cars
if (itemData.place_result && !FD.entities[itemData.place_result]) continue
} else {
@ -194,7 +194,7 @@ export class InventoryDialog extends Dialog {
)
}
/** Update recipe visulaization */
/** Update recipe visualization */
private updateRecipeVisualization(recipeName?: string): void {
// Update Recipe Label
this.m_RecipeLabel.text = undefined

View File

@ -8,9 +8,32 @@ const getRandomInt = (min: number, max: number): number => {
const getRandomItem = <T>(array: T[]): T => array[getRandomInt(0, array.length - 1)]
const Point = (p: IPoint | number[]): IPoint => {
if (Array.isArray(p)) return { x: p[0], y: p[1] }
return { ...p }
}
/** Computes a weighted sum of vectors. Weights are preceding their corresponding vector, and equal to 1 if not specified */
const sumprod = (...args: (number | (number[]|IPoint))[]): IPoint => {
const ans: IPoint = {x:0, y:0}
let coef: number = undefined
for (let arg of args) {
if (typeof arg === 'number') {
coef = (coef ?? 1) * arg
continue
}
if (Array.isArray(arg)) arg = { x: arg[0], y: arg[1] }
ans.x += (coef ?? 1) * arg.x
ans.y += (coef ?? 1) * arg.y
coef = undefined
}
if (coef !== undefined) throw new TypeError("weights should be followed by a vector")
return ans
}
const rotatePointBasedOnDir = (p: IPoint | number[], dir: number): IPoint => {
const point: IPoint = { x: 0, y: 0 }
const nP = Array.isArray(p) ? { x: p[0], y: p[1] } : { ...p }
const nP = Point(p)
switch (dir) {
case 0:
// x y
@ -144,6 +167,8 @@ export default {
duplicate,
getRandomInt,
getRandomItem,
Point,
sumprod,
getRelativeDirection,
rotatePointBasedOnDir,
transformConnectionPosition,

View File

@ -15,6 +15,7 @@ import { OverlayContainer } from './OverlayContainer'
import { PaintEntityContainer } from './PaintEntityContainer'
import { TileContainer } from './TileContainer'
import { PaintTileContainer } from './PaintTileContainer'
import { PaintWireContainer } from './PaintWireContainer'
import { PaintContainer } from './PaintContainer'
import { PaintBlueprintContainer } from './PaintBlueprintContainer'
import { OptimizedContainer } from './OptimizedContainer'
@ -60,9 +61,10 @@ export class BlueprintContainer extends PIXI.Container {
3
)
private _moveSpeed = 10
private _gridColor = 0x303030
private _gridPattern: GridPattern = 'grid'
private _moveSpeed = Number(localStorage.getItem('moveSpeed') ?? 10)
private _gridColor = (localStorage.getItem('darkTheme') ?? 'true') === 'true' ? 0x303030 : 0xc9c9c9
private _gridPattern: GridPattern = (localStorage.getItem('pattern') ?? 'grid') as GridPattern
private _limitWireReach = (localStorage.getItem('limitWireReach') ?? 'true') === 'true'
private _mode: EditorMode = EditorMode.NONE
public readonly bp: Blueprint
public readonly gridData: GridData
@ -77,6 +79,7 @@ export class BlueprintContainer extends PIXI.Container {
public readonly wiresContainer: WiresContainer
public readonly overlayContainer: OverlayContainer
private readonly entityPaintSlot: PIXI.Container
private readonly wirePaintSlot: PIXI.Container
public hoverContainer: EntityContainer
public paintContainer: PaintContainer
@ -114,6 +117,7 @@ export class BlueprintContainer extends PIXI.Container {
this.wiresContainer = new WiresContainer(this.bp)
this.overlayContainer = new OverlayContainer(this)
this.entityPaintSlot = new PIXI.Container()
this.wirePaintSlot = new PIXI.Container()
this.addChild(
this.grid,
@ -124,7 +128,8 @@ export class BlueprintContainer extends PIXI.Container {
this.entitySprites,
this.wiresContainer,
this.overlayContainer,
this.entityPaintSlot
this.entityPaintSlot,
this.wirePaintSlot
)
this.on('pointerover', () => {
@ -439,10 +444,10 @@ export class BlueprintContainer extends PIXI.Container {
if (!this.bp) return
const entity = this.bp.entityPositionGrid.getEntityAtPosition(
this.gridData.x32,
this.gridData.y32
)
const entity = this.bp.entityPositionGrid.getEntityAtPosition({
x: this.gridData.x32,
y: this.gridData.y32
})
const eC = entity ? EntityContainer.mappings.get(entity.entityNumber) : undefined
if (eC && this.hoverContainer === eC) return
@ -555,6 +560,14 @@ export class BlueprintContainer extends PIXI.Container {
return grid
}
public get limitWireReach(): boolean {
return this._limitWireReach
}
public set limitWireReach(limit: boolean) {
this._limitWireReach = limit
}
public initBP(): void {
// Render Bp
for (const [, e] of this.bp.entities) {
@ -706,10 +719,14 @@ export class BlueprintContainer extends PIXI.Container {
if (typeof itemNameOrEntities === 'string') {
const itemData = FD.items[itemNameOrEntities]
const wireResult = itemData.wire_count && itemNameOrEntities
const tileResult = itemData.place_as_tile && itemData.place_as_tile.result
const placeResult = itemData.place_result || tileResult
const placeResult = itemData.place_result || tileResult || wireResult
if (tileResult) {
if (wireResult) {
this.paintContainer = new PaintWireContainer(this, placeResult)
this.wirePaintSlot.addChild(this.paintContainer)
} else if (tileResult) {
this.paintContainer = new PaintTileContainer(this, placeResult)
this.tilePaintSlot.addChild(this.paintContainer)
} else {

View File

@ -301,13 +301,7 @@ export class OverlayContainer extends PIXI.Container {
case 6:
position.x += offset
}
const arrow = createArrow(
{
x: position.x * 64,
y: position.y * 64,
},
type
)
const arrow = createArrow(util.sumprod(64,position), type)
if (entity.type === 'boiler' && type === 2) {
arrow.rotation = 0.5 * Math.PI
}

View File

@ -163,15 +163,17 @@ export class PaintBlueprintContainer extends PaintContainer {
for (const [oldID] of oldEntIDToNewEntID) {
this.bp.wireConnections
.getEntityConnections(oldID)
.filter(
connection =>
oldEntIDToNewEntID.has(connection.entityNumber1) &&
oldEntIDToNewEntID.has(connection.entityNumber2)
.filter(connection =>
connection.cps.every(cp =>
oldEntIDToNewEntID.has(cp.entityNumber)
)
)
.map(connection => ({
...connection,
entityNumber1: oldEntIDToNewEntID.get(connection.entityNumber1),
entityNumber2: oldEntIDToNewEntID.get(connection.entityNumber2),
cps: connection.cps.map(cp => ({
...cp,
entityNumber: oldEntIDToNewEntID.get(cp.entityNumber),
}))
}))
.forEach(conn => this.bpc.bp.wireConnections.create(conn))
}

View File

@ -1,6 +1,7 @@
import F from '../UI/controls/functions'
import { Entity } from '../core/Entity'
import { Blueprint } from '../core/Blueprint'
import util from '../common/util'
import { EntitySprite } from './EntitySprite'
import { VisualizationArea } from './VisualizationArea'
import { BlueprintContainer } from './BlueprintContainer'
@ -31,26 +32,17 @@ export class PaintBlueprintEntityContainer {
this.entitySprites = EntitySprite.getParts(
this.entity,
{
x: this.entity.position.x * 32,
y: this.entity.position.y * 32,
},
util.sumprod(32,this.entity.position),
this.bp.entityPositionGrid
)
}
private get entityPosition(): IPoint {
return {
x: this.pbpc.x / 32 + this.entity.position.x,
y: this.pbpc.y / 32 + this.entity.position.y,
}
return util.sumprod(1/32,this.pbpc, this.entity.position)
}
private get position(): IPoint {
return {
x: this.pbpc.x + this.entity.position.x,
y: this.pbpc.y + this.entity.position.y,
}
return util.sumprod(this.pbpc, this.entity.position)
}
public destroy(): void {
@ -73,7 +65,7 @@ export class PaintBlueprintEntityContainer {
direction,
position
) ||
this.bpc.bp.entityPositionGrid.isAreaAvalible(this.entity.name, position, direction)
this.bpc.bp.entityPositionGrid.isAreaAvailable(this.entity.name, position, direction)
for (const s of this.entitySprites) {
F.applyTint(s, {
@ -142,7 +134,7 @@ export class PaintBlueprintEntityContainer {
}
let ent: Entity
if (this.bpc.bp.entityPositionGrid.isAreaAvalible(this.entity.name, position, direction)) {
if (this.bpc.bp.entityPositionGrid.isAreaAvailable(this.entity.name, position, direction)) {
ent = this.bpc.bp.createEntity({
...this.entity.serialize(),
entity_number: undefined,

View File

@ -13,7 +13,8 @@ export class IllegalFlipError {
export abstract class PaintContainer extends PIXI.Container {
protected readonly bpc: BlueprintContainer
private readonly icon: PIXI.DisplayObject
private _name: string
private icon: PIXI.DisplayObject
private _blocked = false
private tint = {
r: 0.4,
@ -29,12 +30,22 @@ export abstract class PaintContainer extends PIXI.Container {
this.on('childAdded', (s: PIXI.Sprite) => F.applyTint(s, this.tint))
this.icon = F.CreateIcon(this.getItemName())
G.UI.addPaintIcon(this.icon)
window.addEventListener('mousemove', this.updateIconPos)
this.show()
}
public get name(): string {
return this._name
}
protected set name(name: string) {
this._name = name
this.icon?.destroy()
this.icon = F.CreateIcon(this.getItemName())
G.UI.addPaintIcon(this.icon)
this.updateIconPos()
}
protected get blocked(): boolean {
return this._blocked
}
@ -73,15 +84,19 @@ export abstract class PaintContainer extends PIXI.Container {
}
}
protected setNewPosition(size: IPoint): void {
if (size.x % 2 === 0) {
protected setNewPosition(size?: IPoint): void {
if (size === undefined) {
this.x = this.bpc.gridData.x
} else if (size.x % 2 === 0) {
const npx = this.bpc.gridData.x16 * 16
this.x = npx + (npx % 32 === 0 ? 0 : 16)
} else {
this.x = this.bpc.gridData.x32 * 32 + 16
}
if (size.y % 2 === 0) {
if (size === undefined) {
this.y = this.bpc.gridData.y
} else if (size.y % 2 === 0) {
const npy = this.bpc.gridData.y16 * 16
this.y = npy + (npy % 32 === 0 ? 0 : 16)
} else {

View File

@ -70,7 +70,7 @@ export class PaintEntityContainer extends PaintContainer {
direction,
position
) ||
this.bpc.bp.entityPositionGrid.isAreaAvalible(this.name, position, direction)
this.bpc.bp.entityPositionGrid.isAreaAvailable(this.name, position, direction)
) {
this.blocked = false
} else {
@ -206,7 +206,7 @@ export class PaintEntityContainer extends PaintContainer {
return
}
if (this.bpc.bp.entityPositionGrid.isAreaAvalible(this.name, position, direction)) {
if (this.bpc.bp.entityPositionGrid.isAreaAvailable(this.name, position, direction)) {
this.bpc.bp.createEntity(
{
name: this.name,

View File

@ -1,9 +1,9 @@
import FD from '../core/factorioData'
import { Tile } from '../core/Tile'
import util from '../common/util'
import { TileContainer } from './TileContainer'
import { PaintContainer } from './PaintContainer'
import { BlueprintContainer } from './BlueprintContainer'
import { Entity } from '../core/Entity'
export class PaintTileContainer extends PaintContainer {
private static size = 2
@ -60,7 +60,7 @@ export class PaintTileContainer extends PaintContainer {
this.redraw()
}
public rotate(): void {
public rotate(ccw = false): void {
const nD = FD.tiles[this.name].next_direction
if (nD) {
this.name = nD
@ -68,8 +68,8 @@ export class PaintTileContainer extends PaintContainer {
}
}
public rotatedEntities(): Entity[] {
return undefined
public canFlipOrRotateByCopying(): boolean {
return false;
}
protected redraw(): void {
@ -95,10 +95,9 @@ export class PaintTileContainer extends PaintContainer {
const position = this.getGridPosition()
this.bpc.bp.removeTiles(
PaintTileContainer.getTilePositions().map(p => ({
x: p.x + position.x,
y: p.y + position.y,
}))
PaintTileContainer.getTilePositions().map(p =>
util.sumprod(p, position)
)
)
}
@ -109,10 +108,9 @@ export class PaintTileContainer extends PaintContainer {
this.bpc.bp.createTiles(
this.name,
PaintTileContainer.getTilePositions().map(p => ({
x: p.x + position.x,
y: p.y + position.y,
}))
PaintTileContainer.getTilePositions().map(p =>
util.sumprod(p, position)
)
)
}
}

View File

@ -0,0 +1,175 @@
import * as PIXI from 'pixi.js'
import G from '../common/globals'
import U from '../core/generators/util'
import { IConnection, IConnectionPoint } from '../core/WireConnections'
import { Entity } from '../core/Entity'
import { EntityContainer } from './EntityContainer'
import { PaintContainer } from './PaintContainer'
import { BlueprintContainer } from './BlueprintContainer'
export class PaintWireContainer extends PaintContainer {
private color? : string
private cp? : IConnectionPoint? = undefined
/** This is only a reference */
private cursorBox: PIXI.Container
public constructor(bpc: BlueprintContainer, name: string) {
super(bpc, name)
this.color = name.split("_",1)[0]
this.cp = undefined
this.moveAtCursor()
this.redraw()
}
private get entity(): Entity {
if (this.cp === undefined) return undefined
return this.bpc.bp.entities.get(this.cp.entityNumber)
}
public hide(): void {
super.hide()
}
public show(): void {
super.hide() // keep icon visible
this.visible = true
}
public destroy(pipette = false): void {
if (pipette && this.cp) {
this.cp = undefined
this.redraw()
return
}
this.bpc.wiresContainer.remove('paint-wire')
this.destroycursorBox()
super.destroy()
}
public getItemName(): string {
return this.name
}
private updatecursorBox(): IConnectionPoint {
this.destroycursorBox()
const cursor_position = this.getGridPosition()
const entity = this.bpc.bp.entityPositionGrid.getEntityAtPosition(cursor_position)
if (entity === undefined) return undefined
const ec = EntityContainer.mappings.get(entity.entityNumber)
const cp = this.bpc.bp.entityPositionGrid.getConnectionPointAtPosition(cursor_position, this.color)
let connectionsReach = true
if (this.cp && G.BPC.limitWireReach) {
connectionsReach &&= U.pointInCircle(
entity.position,
this.cp.position ?? this.entity.position,
Math.min(
entity.maxWireDistance ?? Infinity,
this.entity?.maxWireDistance ?? Infinity
)
)
}
this.cursorBox = this.bpc.overlayContainer.createCursorBox(
ec.position,
entity.size,
cp === undefined ? 'not_allowed'
: (!connectionsReach) ? 'not_allowed'
: 'regular'
)
if (connectionsReach) return cp
}
private destroycursorBox(): void {
this.cursorBox?.destroy()
}
public rotate(ccw = false): void {
if (!this.visible) return
// const cursor_position = this.getGridPosition()
// const entity = this.bpc.bp.entityPositionGrid.getEntityAtPosition(cursor_position)
// entity?.rotate(ccw, true)
/** Non-standard behavior: cycle between colors */
if (this.name === "red_wire") this.name = "green_wire"
else if (this.name === "green_wire") this.name = "red_wire"
this.color = this.name.split("_",1)[0]
this.redraw()
}
public canFlipOrRotateByCopying(): boolean {
return false;
}
protected redraw(): void {
this.updatecursorBox()
this.bpc.wiresContainer.remove('paint-wire')
if (this.cp) {
const connection : IConnection = {
color: this.color,
cps: [this.cp, { position: this.getGridPosition() }],
}
this.bpc.wiresContainer.add('paint-wire', connection)
}
}
public moveAtCursor(): void {
this.setNewPosition()
this.redraw()
}
public placeEntityContainer(): void {
if (!this.visible) return
const cp = this.updatecursorBox()
if (cp === undefined) return
if (this.cp?.entityNumber === undefined) {
this.cp = cp
} else {
const connection : IConnection = {
color: this.color,
cps: [this.cp, cp],
}
if (cp.entityNumber === this.cp.entityNumber && cp.entitySide === this.cp.entitySide) {
this.cp = undefined
} else if (this.bpc.bp.wireConnections.has(connection)) {
this.bpc.bp.wireConnections.remove(connection)
this.cp = undefined
} else {
this.bpc.bp.wireConnections.create(connection)
this.cp = cp
}
}
this.moveAtCursor()
}
/** Non-standard behavior: on right click, keep focusing same connection point. */
public removeContainerUnder(): void {
if (!this.visible) return
const cp = this.updatecursorBox()
if (cp === undefined) return
if (this.cp?.entityNumber === undefined) {
this.cp = cp
} else {
const connection : IConnection = {
color: this.color,
cps: [this.cp, cp],
}
if (cp.entityNumber === this.cp.entityNumber && cp.entitySide === this.cp.entitySide) {
this.cp = undefined
} else if (this.bpc.bp.wireConnections.has(connection)) {
this.bpc.bp.wireConnections.remove(connection)
} else {
this.bpc.bp.wireConnections.create(connection)
}
}
this.moveAtCursor()
}
}

View File

@ -1,6 +1,7 @@
import * as PIXI from 'pixi.js'
import { Blueprint } from '../core/Blueprint'
import { IConnection } from '../core/WireConnections'
import { IConnection, IConnectionPoint } from '../core/WireConnections'
import U from '../core/generators/util'
import { EntityContainer } from './EntityContainer'
export class WiresContainer extends PIXI.Container {
@ -12,7 +13,7 @@ export class WiresContainer extends PIXI.Container {
this.bp = bp
}
private static createWire(p1: IPoint, p2: IPoint, color: string): PIXI.Graphics {
private static createWire(p1: IPoint, p2: IPoint, color: string, connectionsReach = true): PIXI.Graphics {
const wire = new PIXI.Graphics()
const minX = Math.min(p1.x, p2.x)
@ -28,7 +29,11 @@ export class WiresContainer extends PIXI.Container {
green: 0x588c38,
}
wire.lineStyle({ width: 1.5, color: colorMap[color] })
wire.lineStyle({
width: 1.5,
color: colorMap[color],
alpha: (connectionsReach ? 1 : 0.3),
})
wire.moveTo(0, 0)
if (p1.x === p2.x) {
@ -94,7 +99,7 @@ export class WiresContainer extends PIXI.Container {
for (const conn of connections) {
const entNr =
entityNumber === conn.entityNumber1 ? conn.entityNumber2 : conn.entityNumber1
entityNumber === conn.cps[0].entityNumber ? conn.cps[1].entityNumber : conn.cps[0].entityNumber
const ec = EntityContainer.mappings.get(entNr)
if (ec.entity.type === 'electric_pole') {
ec.redraw()
@ -106,12 +111,11 @@ export class WiresContainer extends PIXI.Container {
}
private updateConnectedEntities(connection: IConnection): void {
const ent0 = EntityContainer.mappings.get(connection.entityNumber1)
const ent1 = EntityContainer.mappings.get(connection.entityNumber2)
ent0.redraw()
ent1.redraw()
this.update(connection.entityNumber1)
this.update(connection.entityNumber2)
for (const cp of connection.cps) {
const ec = EntityContainer.mappings.get(cp.entityNumber)
ec.redraw()
this.update(cp.entityNumber)
}
}
/** This is done in cases where the connection doesn't change but the rotation does */
@ -125,19 +129,46 @@ export class WiresContainer extends PIXI.Container {
}
private getWireSprite(connection: IConnection): PIXI.Graphics {
const getWirePos = (entityNumber: number, color: string, side: number): IPoint => {
const entity = this.bp.entities.get(entityNumber)
const point = entity.getWireConnectionPoint(color, side, entity.direction)
return {
x: (entity.position.x + point[0]) * 32,
y: (entity.position.y + point[1]) * 32,
const getWirePos = (cp: IConnectionPoint, color: string): IPoint => {
if (cp.entityNumber) {
const entity = this.bp.entities.get(cp.entityNumber)
const point = entity.getWireConnectionPoint(color, cp.entitySide)
return {
x: (entity.position.x + point[0]) * 32,
y: (entity.position.y + point[1]) * 32,
}
} else if (cp.position) {
return {
x: cp.position.x * 32,
y: cp.position.y * 32,
}
}
}
const getPos = (cp: IConnectionPoint): IPoint => {
if (cp.entityNumber) {
const entity = this.bp.entities.get(cp.entityNumber)
return entity.position
} else if (cp.position) {
return cp.position
}
}
const getMaxWireDistance = (cp: IConnectionPoint): number => {
if (cp.entityNumber) {
const entity = this.bp.entities.get(cp.entityNumber)
return entity.maxWireDistance
}
}
const connectionsReach = U.pointInCircle(
getPos(connection.cps[0]),
getPos(connection.cps[1]),
Math.min(Infinity, ...connection.cps.map(getMaxWireDistance).filter(d => d !== undefined))
)
return WiresContainer.createWire(
getWirePos(connection.entityNumber1, connection.color, connection.entitySide1),
getWirePos(connection.entityNumber2, connection.color, connection.entitySide2),
connection.color
getWirePos(connection.cps[0], connection.color),
getWirePos(connection.cps[1], connection.color),
connection.color,
connectionsReach
)
}
}

View File

@ -160,10 +160,7 @@ class Blueprint extends EventEmitter {
delete e.neighbours
return this.createEntity({
...e,
position: {
x: e.position.x + offset.x,
y: e.position.y + offset.y,
},
position: util.sumprod(e.position, offset),
})
}),
e => e.entityNumber

View File

@ -1,6 +1,7 @@
import { EventEmitter } from 'eventemitter3'
import util from '../common/util'
import { IllegalFlipError } from '../containers/PaintContainer'
import G from '../common/globals'
import FD, { Entity as FD_Entity } from './factorioData'
import { Blueprint } from './Blueprint'
import { getBeltWireConnectionIndex } from './spriteDataBuilder'
@ -70,6 +71,10 @@ export class Entity extends EventEmitter {
return FD.entities[this.name]
}
public get rawEntity(): BPS.IEntity {
return this.m_rawEntity;
}
/** Entity size */
public get size(): IPoint {
return util.switchSizeBasedOnDirection(this.entityData.size, this.direction)
@ -80,28 +85,30 @@ export class Entity extends EventEmitter {
return this.m_rawEntity.position
}
public get rawEntity(): BPS.IEntity {
return this.m_rawEntity;
}
public set position(position: IPoint) {
if (util.areObjectsEquivalent(this.m_rawEntity.position, position)) return
if (!this.m_BP.entityPositionGrid.canMoveTo(this, position)) return
// Make sure all entity connections reach the new position
const connectionsReach = this.m_BP.wireConnections
// Check if the new position breaks any valid entity connections
const connectionsBreak = this.m_BP.wireConnections
.getEntityConnections(this.entityNumber)
.map(c => (c.entityNumber1 === this.entityNumber ? c.entityNumber2 : c.entityNumber1))
.map(c => (c.cps[0].entityNumber === this.entityNumber ? c.cps[1].entityNumber : c.cps[0].entityNumber))
.map(otherEntityNumer => this.m_BP.entities.get(otherEntityNumer))
.every(e =>
.some(e =>
// Make sure that a reaching connection is not broken
U.pointInCircle(
e.position,
this.position,
Math.min(e.maxWireDistance, this.maxWireDistance)
) &&
!U.pointInCircle(
e.position,
position,
Math.min(e.maxWireDistance, this.maxWireDistance)
)
)
if (!connectionsReach) return
if (G.BPC.limitWireReach && connectionsBreak) return
this.m_BP.history
.updateValue(this.m_rawEntity, ['position'], position, 'Change position')
@ -121,11 +128,22 @@ export class Entity extends EventEmitter {
)
}
public connectionsReach(position?: IPoint): boolean {
return this.m_BP.wireConnections
.getEntityConnections(this.entityNumber)
.map(c => (c.cps[0].entityNumber === this.entityNumber ? c.cps[1].entityNumber : c.cps[0].entityNumber))
.map(otherEntityNumer => this.m_BP.entities.get(otherEntityNumer))
.every(e =>
U.pointInCircle(
e.position,
position ?? this.position,
Math.min(e.maxWireDistance, this.maxWireDistance)
)
)
}
public moveBy(offset: IPoint): void {
this.position = {
x: this.position.x + offset.x,
y: this.position.y + offset.y,
}
this.position = util.sumprod(this.position, offset)
}
/** Entity direction */
@ -870,6 +888,50 @@ export class Entity extends EventEmitter {
return e.circuit_wire_connection_points[getIndex()].wire[color]
}
private getWire_connection_box(
color: string,
side: number,
direction = this.direction
): number[][] {
const e = this.entityData
const size_box = [
[-e.size.width/2, -e.size.height/2],
[+e.size.width/2, +e.size.height/2],
]
// use size_box for cell-wise selection, use e.selection_box for "true" selection
if (side===1 && e.connection_points?.[direction/2].wire[color]) return size_box
if (side===1 && e.circuit_wire_connection_point?.wire[color]) return size_box
if (side===1 && e.circuit_wire_connection_points?.[direction/2].wire[color]) return size_box
if (side===1 && e.input_connection_points?.[direction/2].wire[color]) return e.input_connection_bounding_box
if (side===2 && e.output_connection_points?.[direction/2].wire[color]) return e.output_connection_bounding_box
if (side===1 && e.left_wire_connection_point?.wire[color]) {
const box = util.duplicate(size_box)
box[1][0] = (box[0][0]+box[1][0])/2
return box
}
if (side===2 && e.right_wire_connection_point?.wire[color]) {
const box = util.duplicate(size_box)
box[0][0] = (box[0][0]+box[1][0])/2
return box
}
}
public getWireConnectionBoundingBox(
color: string,
side: number,
direction = this.direction
): IPoint[] {
const box = this.getWire_connection_box(color, side, direction)
if (box === undefined) return undefined
let bbox : IPoint[] = box.map(util.Point)
bbox = bbox.map(p => util.rotatePointBasedOnDir(p, direction))
bbox = [
{x: Math.min(...bbox.map(p => p.x)), y: Math.min(...bbox.map(p => p.y))},
{x: Math.max(...bbox.map(p => p.x)), y: Math.max(...bbox.map(p => p.y))},
]
return bbox
}
public serialize(entNrWhitelist?: Set<number>): BPS.IEntity {
return util.duplicate({
...this.m_rawEntity,

View File

@ -2,6 +2,7 @@ import util from '../common/util'
import FD from './factorioData'
import { Blueprint } from './Blueprint'
import { Entity } from './Entity'
import { IConnectionPoint } from './WireConnections'
/** Anchor is in the middle */
interface IArea {
@ -56,8 +57,8 @@ export class PositionGrid {
}
}
public getEntityAtPosition(x: number, y: number): Entity {
const cell = this.grid.get(`${Math.floor(x)},${Math.floor(y)}`)
public getEntityAtPosition(position: IPoint): Entity {
const cell = this.grid.get(`${Math.floor(position.x)},${Math.floor(position.y)}`)
if (cell) {
if (typeof cell === 'number') {
return this.bp.entities.get(cell)
@ -67,6 +68,23 @@ export class PositionGrid {
}
}
public getConnectionPointAtPosition(position: IPoint, color: string): IConnectionPoint {
const entity = this.getEntityAtPosition(position)
if (entity === undefined) return undefined
const rel_position = util.sumprod(position, -1,entity.position)
for (let side = 1; side <= 10; side++) {
const bbox = entity.getWireConnectionBoundingBox(color, side)
if (bbox === undefined) break // no more sides expected for that color
const rel_bbox = bbox.map(b => util.sumprod(rel_position, -1,b))
if (Object.values(rel_bbox[0]).some(v => v < 0)) continue
if (Object.values(rel_bbox[1]).some(v => v > 0)) continue
return {
entityNumber: entity.entityNumber,
entitySide: side,
}
}
}
public setTileData(entity: Entity, position: IPoint = entity.position): void {
// if (entity.entityData.flags.includes('placeable_off_grid')) {
// return
@ -138,12 +156,12 @@ export class PositionGrid {
public canMoveTo(entity: Entity, newPosition: IPoint): boolean {
this.removeTileData(entity)
const spaceAvalible = this.isAreaAvalible(entity.name, newPosition, entity.direction)
const spaceAvalible = this.isAreaAvailable(entity.name, newPosition, entity.direction)
this.setTileData(entity)
return spaceAvalible
}
public isAreaAvalible(name: string, pos: IPoint, direction = 0): boolean {
public isAreaAvailable(name: string, pos: IPoint, direction = 0): boolean {
const size = util.switchSizeBasedOnDirection(FD.entities[name].size, direction)
const straightRails: Entity[] = []

View File

@ -16,9 +16,9 @@ export class WireConnectionMap extends Map<string, IConnection> {
const conn = this.entNrToConnHash.get(entityNumber) || []
this.entNrToConnHash.set(entityNumber, [...conn, hash])
}
add(connection.entityNumber1)
if (connection.entityNumber1 !== connection.entityNumber2) {
add(connection.entityNumber2)
add(connection.cps[0].entityNumber)
if (connection.cps[0].entityNumber !== connection.cps[1].entityNumber) {
add(connection.cps[1].entityNumber)
}
return super.set(hash, connection)
@ -34,9 +34,9 @@ export class WireConnectionMap extends Map<string, IConnection> {
this.entNrToConnHash.delete(entityNumber)
}
}
rem(connection.entityNumber1)
if (connection.entityNumber1 !== connection.entityNumber2) {
rem(connection.entityNumber2)
rem(connection.cps[0].entityNumber)
if (connection.cps[0].entityNumber !== connection.cps[1].entityNumber) {
rem(connection.cps[1].entityNumber)
}
return super.delete(hash)

View File

@ -8,10 +8,13 @@ const MAX_POLE_CONNECTION_COUNT = 5
export interface IConnection {
color: string
entityNumber1: number
entityNumber2: number
entitySide1: number
entitySide2: number
cps: IConnectionPoint[]
}
export interface IConnectionPoint {
entityNumber?: number
entitySide?: number
position?: IPoint
}
export class WireConnections extends EventEmitter {
@ -24,10 +27,9 @@ export class WireConnections extends EventEmitter {
}
private static hash(conn: IConnection): string {
const firstE = Math.min(conn.entityNumber1, conn.entityNumber2)
const secondE = Math.max(conn.entityNumber1, conn.entityNumber2)
const firstS = firstE === conn.entityNumber1 ? conn.entitySide1 : conn.entitySide2
const secondS = secondE === conn.entityNumber2 ? conn.entitySide2 : conn.entitySide1
const cps = conn.cps.sort((cp1,cp2) => cp1.entityNumber - cp2.entityNumber)
const [firstE, secondE] = cps.map(cp => cp.entityNumber)
const [firstS, secondS] = cps.map(cp => cp.entitySide)
return `${conn.color}-${firstE}-${secondE}-${firstS}-${secondS}`
}
@ -46,10 +48,16 @@ export class WireConnections extends EventEmitter {
for (const data of conn[color]) {
parsedConnections.push({
color,
entityNumber1: entityNumber,
entityNumber2: data.entity_id,
entitySide1: Number(side),
entitySide2: data.circuit_id || 1,
cps: [
{
entityNumber: entityNumber,
entitySide: Number(side)
},
{
entityNumber: data.entity_id,
entitySide: data.circuit_id || 1
}
]
})
}
}
@ -62,10 +70,16 @@ export class WireConnections extends EventEmitter {
const data = (connections[side] as BPS.IWireColor[])[0]
parsedConnections.push({
color,
entityNumber1: entityNumber,
entityNumber2: data.entity_id,
entitySide1: Number(side.slice(2, 3)) + 1,
entitySide2: 1,
cps: [
{
entityNumber: entityNumber,
entitySide: Number(side.slice(2, 3)) + 1
},
{
entityNumber: data.entity_id,
entitySide: 1
}
]
})
}
}
@ -97,39 +111,38 @@ export class WireConnections extends EventEmitter {
const neighbours: number[] = []
for (const connection of connections) {
const isEntity1 = connection.entityNumber1 === entityNumber
const side = isEntity1 ? connection.entitySide1 : connection.entitySide2
const isEntity0 = connection.cps[0].entityNumber === entityNumber
const [thisE, otherE] = isEntity0 ? connection.cps : connection.cps.reverse()
const entitySide = thisE.entitySide
const color = connection.color
const otherEntNr = isEntity1 ? connection.entityNumber2 : connection.entityNumber1
const otherEntSide = isEntity1 ? connection.entitySide2 : connection.entitySide1
if (entNrWhitelist && !entNrWhitelist.has(otherEntNr)) continue
if (entNrWhitelist && !entNrWhitelist.has(otherE.entityNumber)) continue
if (color === 'copper' && getType(otherEntNr) === 'electric_pole') {
if (color === 'copper' && getType(otherE.entityNumber) === 'electric_pole') {
if (getType(entityNumber) === 'electric_pole') {
neighbours.push(otherEntNr)
neighbours.push(otherE.entityNumber)
} else if (getType(entityNumber) === 'power_switch') {
const SIDE = `Cu${side - 1}`
const SIDE = `Cu${entitySide - 1}`
if (serialized[SIDE] === undefined) {
serialized[SIDE] = []
}
const c = serialized[SIDE] as BPS.IWireColor[]
c.push({
entity_id: otherEntNr,
entity_id: otherE.entityNumber,
wire_id: 0,
})
}
} else if (color === 'red' || color === 'green') {
if (serialized[side] === undefined) {
serialized[side] = {}
if (serialized[entitySide] === undefined) {
serialized[entitySide] = {}
}
const SIDE = serialized[side] as BPS.IConnSide
const SIDE = serialized[entitySide] as BPS.IConnSide
if (SIDE[color] === undefined) {
SIDE[color] = []
}
SIDE[color].push({
entity_id: otherEntNr,
circuit_id: otherEntSide,
entity_id: otherE.entityNumber,
circuit_id: otherE.entitySide,
})
}
}
@ -143,13 +156,24 @@ export class WireConnections extends EventEmitter {
private static toPoleConnection(entityNumber1: number, entityNumber2: number): IConnection {
return {
color: 'copper',
entityNumber1,
entityNumber2,
entitySide1: 1,
entitySide2: 1,
cps: [
{
entityNumber: entityNumber1,
entitySide: 1
},
{
entityNumber: entityNumber2,
entitySide: 1
}
]
}
}
public has(connection: IConnection): boolean {
const hash = WireConnections.hash(connection)
return this.connections.has(hash)
}
public create(connection: IConnection): void {
const hash = WireConnections.hash(connection)
if (this.connections.has(hash)) return
@ -160,7 +184,7 @@ export class WireConnections extends EventEmitter {
.commit()
}
private remove(connection: IConnection): void {
public remove(connection: IConnection): void {
const hash = WireConnections.hash(connection)
if (!this.connections.has(hash)) return
@ -267,9 +291,9 @@ export class WireConnections extends EventEmitter {
for (const connection of this.getEntityConnections(pole.entityNumber)) {
if (connection.color === 'copper') {
const otherEntNr =
pole.entityNumber === connection.entityNumber1
? connection.entityNumber2
: connection.entityNumber1
pole.entityNumber === connection.cps[0].entityNumber
? connection.cps[1].entityNumber
: connection.cps[0].entityNumber
blacklist.add(otherEntNr)
}
}
@ -360,7 +384,7 @@ export class WireConnections extends EventEmitter {
public getPowerPoleDirection(entityNumber: number): number {
const connections = this.getEntityConnections(entityNumber).map(conn =>
entityNumber === conn.entityNumber1 ? conn.entityNumber2 : conn.entityNumber1
entityNumber === conn.cps[0].entityNumber ? conn.cps[1].entityNumber : conn.cps[0].entityNumber
)
if (connections.length === 0) return 0

View File

@ -227,7 +227,7 @@ function generateCovers(e: FD_Entity, data: IDrawData): SpriteData[] {
y: Math.floor(data.position.y + connection.y),
}
const ent = data.positionGrid.getEntityAtPosition(pos.x, pos.y)
const ent = data.positionGrid.getEntityAtPosition(pos)
if (!ent) return true
if (
@ -547,7 +547,7 @@ function getBeltSprites(
if (d.entity.direction === (d.relDir + 4) % 8) return d
})
const entAtPos = positionGrid.getEntityAtPosition(pos.x, pos.y)
const entAtPos = positionGrid.getEntityAtPosition(pos)
if (
forceStraight ||
entAtPos.type === 'splitter' ||
@ -1077,10 +1077,7 @@ function generateGraphics(e: FD_Entity): (data: IDrawData) => SpriteData[] {
h: size.y,
})
.filter(e => e.name === 'gate')
.map(e => ({
x: e.position.x - data.position.x,
y: e.position.y - data.position.y,
}))
.map(e => util.sumprod(e.position, -1,data.position))
// Rotate relative to mid point
.map(p => util.rotatePointBasedOnDir(p, dir).y)
// Remove duplicates
@ -1240,8 +1237,7 @@ function generateGraphics(e: FD_Entity): (data: IDrawData) => SpriteData[] {
]
.map(o => {
const ent = data.positionGrid.getEntityAtPosition(
data.position.x + o[0],
data.position.y + o[1]
util.sumprod(data.position, o)
)
return !!ent && ent.name === 'stone_wall'
})
@ -1308,10 +1304,10 @@ function generateGraphics(e: FD_Entity): (data: IDrawData) => SpriteData[] {
}
if (data.dir === 0 && data.positionGrid) {
const wall = data.positionGrid.getEntityAtPosition(
data.position.x,
data.position.y + 1
)
const wall = data.positionGrid.getEntityAtPosition({
x: data.position.x,
y: data.position.y + 1
})
if (wall && wall.name === 'stone_wall') {
return [...getBaseSprites(), e.wall_patch.layers[0]]
}
@ -1464,10 +1460,7 @@ function generateGraphics(e: FD_Entity): (data: IDrawData) => SpriteData[] {
const belt0Parts = getBeltSprites(
e.belt_animation_set,
data.positionGrid
? {
x: data.position.x + b0Offset.x,
y: data.position.y + b0Offset.y,
}
? util.sumprod(data.position, b0Offset)
: b0Offset,
data.dir,
data.positionGrid,
@ -1479,10 +1472,7 @@ function generateGraphics(e: FD_Entity): (data: IDrawData) => SpriteData[] {
const belt1Parts = getBeltSprites(
e.belt_animation_set,
data.positionGrid
? {
x: data.position.x + b1Offset.x,
y: data.position.y + b1Offset.y,
}
? util.sumprod(data.position, b1Offset)
: b1Offset,
data.dir,
data.positionGrid,

View File

@ -96,6 +96,17 @@ export function initSettingsPane(
}
editor.debug = debug
})
if (localStorage.getItem('limitWireReach')) {
const limitWireReach = localStorage.getItem('limitWireReach') === 'true'
editor.limitWireReach = limitWireReach
}
gui.add({ limitWireReach: editor.limitWireReach }, 'limitWireReach')
.name('Limit Wires Length')
.onChange((limitWireReach: boolean) => {
localStorage.setItem('limitWireReach', limitWireReach.toString())
editor.limitWireReach = limitWireReach
})
if (localStorage.getItem('oilOutpostSettings')) {
const settings = JSON.parse(localStorage.getItem('oilOutpostSettings'))