Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"main": "src/index.ts",
"scripts": {
"check": "tsc --noEmit",
"test": "tsx test/wires.test.ts && tsx test/wireEdit.test.ts && tsx test/beltAnimation.test.ts && tsx test/animProfile.test.ts && tsx test/pipe.test.ts && tsx test/beltGraph.test.ts && tsx test/spatial-camera.test.ts && tsx test/redraw.test.ts && tsx test/recordingSummary.test.ts && tsx test/entityPreview.test.ts && tsx test/heatNetwork.test.ts && tsx test/inserterLayering.test.ts && tsx test/bake.test.ts && tsx test/undergroundPairing.test.ts && tsx test/buildOver.test.ts && tsx test/fusionPorts.test.ts && tsx test/fluidArt.test.ts && tsx test/railGeometry.test.ts && tsx test/railArt.test.ts && tsx test/railPlanner.test.ts && tsx test/collision.test.ts && tsx test/railBlocks.test.ts && tsx test/railSignals.test.ts && tsx test/tiles.test.ts && tsx test/cargoBay.test.ts && tsx test/sceneCache.test.ts"
"test": "tsx test/wires.test.ts && tsx test/wireEdit.test.ts && tsx test/beltAnimation.test.ts && tsx test/animProfile.test.ts && tsx test/pipe.test.ts && tsx test/beltGraph.test.ts && tsx test/spatial-camera.test.ts && tsx test/redraw.test.ts && tsx test/recordingSummary.test.ts && tsx test/entityPreview.test.ts && tsx test/heatNetwork.test.ts && tsx test/inserterLayering.test.ts && tsx test/bake.test.ts && tsx test/undergroundPairing.test.ts && tsx test/buildOver.test.ts && tsx test/fusionPorts.test.ts && tsx test/fluidArt.test.ts && tsx test/railGeometry.test.ts && tsx test/railArt.test.ts && tsx test/railPlanner.test.ts && tsx test/collision.test.ts && tsx test/railBlocks.test.ts && tsx test/railSignals.test.ts && tsx test/tiles.test.ts && tsx test/cargoBay.test.ts && tsx test/sceneCache.test.ts && tsx test/pasteSnap.test.ts"
},
"dependencies": {
"@factoriotools/engine": "*"
Expand Down
13 changes: 13 additions & 0 deletions packages/renderer/src/pasteSnap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** Where a paste ghost's anchor lands for a cursor at `cursor`, when the
* ghost snaps to whole tiles: the copied position shifted by a whole number
* of tiles. The anchor is the group's centre, which is NOT on a tile corner
* for a group of odd width (one belt, one assembler: x.5), and rounding it
* to a whole tile would slide the stamp half a tile off the grid hand
* placement uses. Keeping the anchor's own fraction keeps every stamped
* entity exactly as far from the grid as the original was. */
export function snapAnchorToTiles(cursor: { x: number; y: number }, anchor: { x: number; y: number }): { x: number; y: number } {
return {
x: anchor.x + Math.round(cursor.x - anchor.x),
y: anchor.y + Math.round(cursor.y - anchor.y),
};
}
4 changes: 3 additions & 1 deletion packages/renderer/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { drawInserter } from "./sprites/inserter.js";
import { SpatialIndex, type IndexedBox } from "./spatialIndex.js";
import { boxHitsEntity, entitiesCollide } from "./collision.js";
import { RAIL_BLOCK_COLORS } from "./railBlocks.js";
import { snapAnchorToTiles } from "./pasteSnap.js";
import { isElevatedRail, isRail, isRollingStock, onRailGrid, openEnds, RAIL_DECK_HEIGHT, RAIL_END_CAP, railCentreline, railHighlightBox, railJoints, snapToRailGrid, type RailEnd, type RailPiece, type RailSlot } from "./railGeometry.js";
import {
buildRailIndex,
Expand Down Expand Up @@ -853,7 +854,8 @@ export function mountRenderer(container: HTMLElement, data: GameData, catalog: R

function snapPasteAnchorToTiles(cursor: { x: number; y: number }): { x: number; y: number } {
const cell = pasteCell();
if (!cell || mode.kind !== "paste" || !mode.snap) return { x: Math.round(cursor.x), y: Math.round(cursor.y) };
if (mode.kind !== "paste") return { x: Math.round(cursor.x), y: Math.round(cursor.y) };
if (!cell || !mode.snap) return snapAnchorToTiles(cursor, mode.anchor);
const { offset, size } = cell;
const origin = mode.snap.absolute ? mode.snap.offset : relativeSnapOrigin;
// The cell's top-left if the ghost were centred on the cursor.
Expand Down
54 changes: 54 additions & 0 deletions packages/renderer/test/pasteSnap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import { snapAnchorToTiles } from "../src/pasteSnap.js";

let passed = 0;
function test(name: string, fn: () => void) {
try {
fn();
passed++;
console.log(` ok ${name}`);
} catch (error) {
console.error(`FAIL ${name}`);
console.error(error);
process.exitCode = 1;
}
}

/** Where hand placement puts a footprint's centre for a cursor (render.ts's
* snapAxis): odd widths on a tile centre, even widths on a tile corner. */
const handPlaced = (cursor: number, width: number) => (width % 2 === 1 ? Math.floor(cursor) + 0.5 : Math.round(cursor));

// Issue #61: copy one building, stamp the ghost anywhere, and it must sit
// where the same building would if placed by hand.
for (const width of [1, 2, 3, 4, 5]) {
test(`${width}x${width} copy stamps where hand placement would`, () => {
const copiedAt = handPlaced(10.3, width);
for (let i = -70; i <= 70; i++) {
const cursor = i / 10 + 0.003;
const snapped = snapAnchorToTiles({ x: cursor, y: cursor }, { x: copiedAt, y: copiedAt });
assert.ok(Math.abs(snapped.x - handPlaced(cursor, width)) < 1e-9, `cursor ${cursor}: ${snapped.x} vs ${handPlaced(cursor, width)}`);
assert.ok(Math.abs(snapped.y - handPlaced(cursor, width)) < 1e-9);
}
});
}

test("a group moves by whole tiles from where it was copied", () => {
const anchor = { x: 1.75, y: -3.5 };
for (let i = -13; i <= 13; i++) {
const cx = i * 0.37;
const snapped = snapAnchorToTiles({ x: cx, y: cx * 1.3 }, anchor);
assert.ok(Math.abs(snapped.x - anchor.x - Math.round(snapped.x - anchor.x)) < 1e-9);
assert.ok(Math.abs(snapped.y - anchor.y - Math.round(snapped.y - anchor.y)) < 1e-9);
}
});

test("the ghost stays within half a tile of the cursor", () => {
const anchor = { x: 0.5, y: 0.5 };
for (let i = -38; i <= 38; i++) {
const c = i * 0.13;
const snapped = snapAnchorToTiles({ x: c, y: c }, anchor);
assert.ok(Math.abs(snapped.x - c) <= 0.5 + 1e-9);
}
});

console.log(`${passed} passed`);
Loading