diff --git a/.timetracker b/.timetracker
index f9eb966..06142b9 100644
--- a/.timetracker
+++ b/.timetracker
@@ -1,10 +1,25 @@
{
- "total": 123,
+ "total": 11122,
"sessions": [
{
"begin": "2026-03-17T23:54:43+01:00",
"end": "2026-03-17T23:56:46+01:00",
"duration": 123
+ },
+ {
+ "begin": "2026-03-17T23:56:56+01:00",
+ "end": "2026-03-18T00:27:03+01:00",
+ "duration": 1807
+ },
+ {
+ "begin": "2026-03-18T19:11:43+01:00",
+ "end": "2026-03-18T21:24:21+01:00",
+ "duration": 7958
+ },
+ {
+ "begin": "2026-03-18T22:54:14+01:00",
+ "end": "2026-03-18T23:14:49+01:00",
+ "duration": 1234
}
]
}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/BlockEntities/BlockEntityButterflyPinBoard.cs b/ButterflyPins/ButterflyPins/BlockEntities/BlockEntityButterflyPinBoard.cs
new file mode 100644
index 0000000..87dd8d3
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/BlockEntities/BlockEntityButterflyPinBoard.cs
@@ -0,0 +1,202 @@
+using System;
+using Vintagestory.API.Client;
+using Vintagestory.API.Common;
+using Vintagestory.API.Datastructures;
+using Vintagestory.API.MathTools;
+using Vintagestory.GameContent;
+
+namespace ButterflyPins.BlockEntities;
+
+public class BlockEntityButterflyPinBoard : BlockEntityDisplay
+{
+ private const int MaxBoardSlots = 9;
+ private const float BoardMargin = 1f / 16f;
+ private const float BoardSurface = 1f / 16f;
+ private const string ButterflyPinPrefix = "clothes-butterflypin-";
+
+ private readonly InventoryGeneric inventory;
+
+ public override InventoryBase Inventory => inventory;
+
+ public override string InventoryClassName => "butterflypinboard";
+
+ public override int DisplayedItems => ActiveSlotCount;
+
+ private int Rows => Math.Max(1, Block?.Attributes?["rows"].AsInt(2) ?? 2);
+
+ private int Columns => Math.Max(1, Block?.Attributes?["columns"].AsInt(2) ?? 2);
+
+ private float RenderScale => Block?.Attributes?["renderScale"].AsFloat(0.18f) ?? 0.18f;
+
+ private int ActiveSlotCount => Rows * Columns;
+
+ public BlockEntityButterflyPinBoard()
+ {
+ inventory = new InventoryGeneric(MaxBoardSlots, null, null);
+ }
+
+ public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldForResolving)
+ {
+ base.FromTreeAttributes(tree, worldForResolving);
+ RedrawAfterReceivingTreeAttributes(worldForResolving);
+ }
+
+ public bool TryInsert(IPlayer byPlayer, int slotIndex, ItemSlot activeSlot)
+ {
+ if (!IsValidSlot(slotIndex) || activeSlot.Empty || !CanInsert(slotIndex, activeSlot.Itemstack))
+ return false;
+
+ ItemStack sourceStack = activeSlot.Itemstack;
+ ItemStack placedStack = sourceStack.Clone();
+ placedStack.StackSize = 1;
+ inventory[slotIndex].Itemstack = placedStack;
+
+ activeSlot.TakeOut(1);
+ activeSlot.MarkDirty();
+ inventory[slotIndex].MarkDirty();
+ MarkChanged();
+ return true;
+ }
+
+ public bool TryTake(IPlayer byPlayer, int slotIndex)
+ {
+ if (!CanTake(slotIndex))
+ return false;
+
+ ItemSlot slot = inventory[slotIndex];
+ ItemStack stack = slot.Itemstack!;
+ slot.Itemstack = null;
+ slot.MarkDirty();
+
+ if (!byPlayer.InventoryManager.TryGiveItemstack(stack, true))
+ Api.World.SpawnItemEntity(stack, Pos.ToVec3d().Add(0.5, 0.5, 0.5));
+
+ MarkChanged();
+ return true;
+ }
+
+ public bool CanInsert(int slotIndex, ItemStack stack)
+ {
+ return IsValidSlot(slotIndex) && inventory[slotIndex].Empty && IsButterflyPin(stack);
+ }
+
+ public bool CanTake(int slotIndex)
+ {
+ return IsValidSlot(slotIndex) && !inventory[slotIndex].Empty;
+ }
+
+ public void DropContents()
+ {
+ for (int slotIndex = 0; slotIndex < ActiveSlotCount; slotIndex++)
+ {
+ ItemSlot slot = inventory[slotIndex];
+ ItemStack? stack = slot.Itemstack;
+ if (stack == null)
+ continue;
+
+ Api.World.SpawnItemEntity(stack, Pos.ToVec3d().Add(0.5, 0.5, 0.5));
+ slot.Itemstack = null;
+ slot.MarkDirty();
+ }
+ }
+
+ public int GetSlotIndex(Vec3d hitPosition)
+ {
+ Vec3d local = ToBaseOrientation(hitPosition);
+ if (local.X < BoardMargin || local.X > 1 - BoardMargin || local.Y < BoardMargin || local.Y > 1 - BoardMargin)
+ return -1;
+
+ double width = (1 - (BoardMargin * 2)) / Columns;
+ double height = (1 - (BoardMargin * 2)) / Rows;
+
+ int column = GameMath.Clamp((int)((local.X - BoardMargin) / width), 0, Columns - 1);
+ int rowFromBottom = GameMath.Clamp((int)((local.Y - BoardMargin) / height), 0, Rows - 1);
+ int rowFromTop = Rows - 1 - rowFromBottom;
+
+ return (rowFromTop * Columns) + column;
+ }
+
+ private bool IsValidSlot(int slotIndex)
+ {
+ return slotIndex >= 0 && slotIndex < ActiveSlotCount;
+ }
+
+ private bool IsButterflyPin(ItemStack stack)
+ {
+ AssetLocation? code = stack.Collectible?.Code;
+ return code != null && code.Path.StartsWith(ButterflyPinPrefix, StringComparison.Ordinal);
+ }
+
+ private void MarkChanged()
+ {
+ RedrawAfterReceivingTreeAttributes(Api.World);
+ base.MarkDirty(true);
+ Api.World.BlockAccessor.MarkBlockDirty(Pos);
+ }
+
+ private Vec3d ToBaseOrientation(Vec3d hitPosition)
+ {
+ Vec3d centered = new(hitPosition.X - 0.5, hitPosition.Y, hitPosition.Z - 0.5);
+ float angle = -GetRotationY();
+ float sin = GameMath.Sin(angle);
+ float cos = GameMath.Cos(angle);
+
+ return new Vec3d(
+ (centered.X * cos) - (centered.Z * sin) + 0.5,
+ hitPosition.Y,
+ (centered.X * sin) + (centered.Z * cos) + 0.5
+ );
+ }
+
+ protected override float[][] genTransformationMatrices()
+ {
+ float[][] matrices = new float[DisplayedItems][];
+ double cellWidth = (1 - (BoardMargin * 2)) / Columns;
+ double cellHeight = (1 - (BoardMargin * 2)) / Rows;
+ float stepX = 0.75f / Columns;
+ float stepY = 0.75f / Rows;
+ float yOffset = (Rows == 2 && Columns == 2 ? 1.0f : 0.75f) + 0.07f;
+ float boardRotationY = GetRotationY();
+ float itemRotationY = boardRotationY - GameMath.PIHALF;
+
+ for (int slotIndex = 0; slotIndex < DisplayedItems; slotIndex++)
+ {
+ int row = slotIndex / Columns;
+ int column = slotIndex % Columns;
+
+ float x = (float)(1 - BoardMargin - (0.5 * cellWidth) - (column * stepX));
+ float y = (float)(BoardMargin + (0.5 * cellHeight) + ((Rows - 1 - row) * stepY));
+ float z = Columns == 2 ? -0/1f : -1/48f;
+ float xOffset = Columns == 2 ? 0.465f : 0.475f;
+ Vec3f off = new(x - xOffset, y - yOffset, z + 0.5f);
+ off = new Matrixf().RotateY(boardRotationY).TransformVector(off.ToVec4f(0)).XYZ;
+
+ matrices[slotIndex] = new Matrixf()
+ .Translate(off.X, off.Y, off.Z)
+ .Translate(0.5f, 0f, 0.5f)
+ .RotateY(itemRotationY)
+ .Scale(RenderScale, RenderScale, RenderScale)
+ .Translate(-0.5f, 0f, -0.5f)
+ .Values;
+ }
+
+ return matrices;
+ }
+
+ public float GetRotationY()
+ {
+ string side = Block?.Variant?["side"] ?? "south";
+ return side switch
+ {
+ "north" => GameMath.PI,
+ "east" => GameMath.PIHALF,
+ "west" => GameMath.PI + GameMath.PIHALF,
+ _ => 0f,
+ };
+ }
+
+ public float GetRenderScale()
+ {
+ return RenderScale;
+ }
+}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/Blocks/BlockButterflyPinBoard.cs b/ButterflyPins/ButterflyPins/Blocks/BlockButterflyPinBoard.cs
new file mode 100644
index 0000000..dbc1bee
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/Blocks/BlockButterflyPinBoard.cs
@@ -0,0 +1,38 @@
+using ButterflyPins.BlockEntities;
+using Vintagestory.API.Common;
+using Vintagestory.API.MathTools;
+
+namespace ButterflyPins.Blocks;
+
+public class BlockButterflyPinBoard : Block
+{
+ public override bool OnBlockInteractStart(IWorldAccessor world, IPlayer byPlayer, BlockSelection blockSel)
+ {
+ if (blockSel == null)
+ return base.OnBlockInteractStart(world, byPlayer, blockSel);
+
+ if (world.BlockAccessor.GetBlockEntity(blockSel.Position) is not BlockEntityButterflyPinBoard board)
+ return base.OnBlockInteractStart(world, byPlayer, blockSel);
+
+ int slotIndex = board.GetSlotIndex(blockSel.HitPosition);
+ if (slotIndex < 0)
+ return base.OnBlockInteractStart(world, byPlayer, blockSel);
+
+ ItemSlot activeSlot = byPlayer.InventoryManager.ActiveHotbarSlot;
+ if (world.Side == EnumAppSide.Client)
+ return (!activeSlot.Empty && board.CanInsert(slotIndex, activeSlot.Itemstack)) || board.CanTake(slotIndex);
+
+ if (!activeSlot.Empty && board.TryInsert(byPlayer, slotIndex, activeSlot))
+ return true;
+
+ return board.TryTake(byPlayer, slotIndex) || base.OnBlockInteractStart(world, byPlayer, blockSel);
+ }
+
+ public override void OnBlockBroken(IWorldAccessor world, BlockPos pos, IPlayer byPlayer, float dropQuantityMultiplier = 1)
+ {
+ if (world.Side == EnumAppSide.Server && world.BlockAccessor.GetBlockEntity(pos) is BlockEntityButterflyPinBoard board)
+ board.DropContents();
+
+ base.OnBlockBroken(world, pos, byPlayer, dropQuantityMultiplier);
+ }
+}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/ButterflyPins.csproj b/ButterflyPins/ButterflyPins/ButterflyPins.csproj
index d61234a..92c3459 100644
--- a/ButterflyPins/ButterflyPins/ButterflyPins.csproj
+++ b/ButterflyPins/ButterflyPins/ButterflyPins.csproj
@@ -75,16 +75,16 @@
-
-
+
+
PreserveNewest
-
-
+
+
-
-
+
+
PreserveNewest
-
-
+
+
diff --git a/ButterflyPins/ButterflyPins/Core/ButterflyPinsModSystem.cs b/ButterflyPins/ButterflyPins/Core/ButterflyPinsModSystem.cs
deleted file mode 100644
index 74d1751..0000000
--- a/ButterflyPins/ButterflyPins/Core/ButterflyPinsModSystem.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Vintagestory.API.Common;
-
-namespace ButterflyPins.Core;
-
-public class ButterflyPinsModSystem : ModSystem
-{
- public override void Start(ICoreAPI api)
- {
- api.Logger.Notification("ButterflyPins loaded");
- }
-}
diff --git a/ButterflyPins/ButterflyPins/Systems/ButterflyPinsModSystem.cs b/ButterflyPins/ButterflyPins/Systems/ButterflyPinsModSystem.cs
new file mode 100644
index 0000000..7a08e40
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/Systems/ButterflyPinsModSystem.cs
@@ -0,0 +1,15 @@
+using ButterflyPins.BlockEntities;
+using ButterflyPins.Blocks;
+using Vintagestory.API.Common;
+
+namespace ButterflyPins.Systems;
+
+public class ButterflyPinsModSystem : ModSystem
+{
+ public override void Start(ICoreAPI api)
+ {
+ api.RegisterBlockClass("BlockButterflyPinBoard", typeof(BlockButterflyPinBoard));
+ api.RegisterBlockEntityClass("ButterflyPinBoard", typeof(BlockEntityButterflyPinBoard));
+ api.Logger.Notification("ButterflyPins loaded");
+ }
+}
diff --git a/ButterflyPins/ButterflyPins/assets/butterflypins/blocktypes/pinboard2x2.json b/ButterflyPins/ButterflyPins/assets/butterflypins/blocktypes/pinboard2x2.json
new file mode 100644
index 0000000..0323535
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/assets/butterflypins/blocktypes/pinboard2x2.json
@@ -0,0 +1,92 @@
+{
+ "code": "pinboard2x2",
+ "class": "BlockButterflyPinBoard",
+ "entityClass": "ButterflyPinBoard",
+ "behaviors": [
+ { "name": "HorizontalAttachable" },
+ { "name": "Lockable" }
+ ],
+ "variantgroups": [
+ { "code": "side", "loadFromProperties": "abstract/horizontalorientation" }
+ ],
+ "attributes": {
+ "rows": 2,
+ "columns": 2,
+ "renderScale": 0.85,
+ "reinforcable": true
+ },
+ "creativeinventory": {
+ "general": ["pinboard2x2-south"],
+ "decorative": ["pinboard2x2-south"]
+ },
+ "shapebytype": {
+ "*-north": { "base": "butterflypins:block/pinboard-2x2", "rotateY": 180 },
+ "*-east": { "base": "butterflypins:block/pinboard-2x2", "rotateY": 90 },
+ "*-south": { "base": "butterflypins:block/pinboard-2x2", "rotateY": 0 },
+ "*-west": { "base": "butterflypins:block/pinboard-2x2", "rotateY": 270 }
+ },
+ "blockmaterial": "Wood",
+ "drawtype": "json",
+ "replaceable": 900,
+ "resistance": 1.5,
+ "lightAbsorption": 0,
+ "faceCullMode": "NeverCull",
+ "sidesolid": { "all": false },
+ "sideopaque": { "all": false },
+ "emitSideAo": { "all": false },
+ "collisionbox": {
+ "x1": 0.0625,
+ "y1": 0.0625,
+ "z1": 0.9375,
+ "x2": 0.9375,
+ "y2": 0.9375,
+ "z2": 1.0,
+ "rotateYByType": {
+ "*-north": 180,
+ "*-east": 90,
+ "*-south": 0,
+ "*-west": 270
+ }
+ },
+ "selectionbox": {
+ "x1": 0.0625,
+ "y1": 0.0625,
+ "z1": 0.9375,
+ "x2": 0.9375,
+ "y2": 0.9375,
+ "z2": 1.0,
+ "rotateYByType": {
+ "*-north": 180,
+ "*-east": 90,
+ "*-south": 0,
+ "*-west": 270
+ }
+ },
+ "combustibleProps": {
+ "burnTemperature": 600,
+ "burnDuration": 20
+ },
+ "sounds": {
+ "place": "block/planks",
+ "break": "block/planks",
+ "hit": "block/planks"
+ },
+ "materialDensity": 400,
+ "guiTransform": {
+ "rotation": { "x": -19, "y": 136, "z": 0 },
+ "origin": { "x": 0.5, "y": 0.5, "z": 0.5 },
+ "scale": 1.9
+ },
+ "groundTransform": {
+ "translation": { "x": 0, "y": 0, "z": 0 },
+ "rotation": { "x": -90, "y": 0, "z": 0 },
+ "origin": { "x": 0.5, "y": 0, "z": 0.5 },
+ "scale": 2.2
+ },
+ "tpHandTransform": {
+ "translation": { "x": -0.45, "y": -0.8, "z": 0.04 },
+ "rotation": { "x": 0, "y": 0, "z": -10 },
+ "origin": { "x": 0.5, "y": 0.5, "z": 0.5 },
+ "scale": 0.95
+ }
+}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/assets/butterflypins/blocktypes/pinboard3x3.json b/ButterflyPins/ButterflyPins/assets/butterflypins/blocktypes/pinboard3x3.json
new file mode 100644
index 0000000..fce72ae
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/assets/butterflypins/blocktypes/pinboard3x3.json
@@ -0,0 +1,92 @@
+{
+ "code": "pinboard3x3",
+ "class": "BlockButterflyPinBoard",
+ "entityClass": "ButterflyPinBoard",
+ "behaviors": [
+ { "name": "HorizontalAttachable" },
+ { "name": "Lockable" }
+ ],
+ "variantgroups": [
+ { "code": "side", "loadFromProperties": "abstract/horizontalorientation" }
+ ],
+ "attributes": {
+ "rows": 3,
+ "columns": 3,
+ "renderScale": 0.675,
+ "reinforcable": true
+ },
+ "creativeinventory": {
+ "general": ["pinboard3x3-south"],
+ "decorative": ["pinboard3x3-south"]
+ },
+ "shapebytype": {
+ "*-north": { "base": "butterflypins:block/pinboard-3x3", "rotateY": 180 },
+ "*-east": { "base": "butterflypins:block/pinboard-3x3", "rotateY": 90 },
+ "*-south": { "base": "butterflypins:block/pinboard-3x3", "rotateY": 0 },
+ "*-west": { "base": "butterflypins:block/pinboard-3x3", "rotateY": 270 }
+ },
+ "blockmaterial": "Wood",
+ "drawtype": "json",
+ "replaceable": 900,
+ "resistance": 1.5,
+ "lightAbsorption": 0,
+ "faceCullMode": "NeverCull",
+ "sidesolid": { "all": false },
+ "sideopaque": { "all": false },
+ "emitSideAo": { "all": false },
+ "collisionbox": {
+ "x1": 0.0625,
+ "y1": 0.0625,
+ "z1": 0.9375,
+ "x2": 0.9375,
+ "y2": 0.9375,
+ "z2": 1.0,
+ "rotateYByType": {
+ "*-north": 180,
+ "*-east": 90,
+ "*-south": 0,
+ "*-west": 270
+ }
+ },
+ "selectionbox": {
+ "x1": 0.0625,
+ "y1": 0.0625,
+ "z1": 0.9375,
+ "x2": 0.9375,
+ "y2": 0.9375,
+ "z2": 1.0,
+ "rotateYByType": {
+ "*-north": 180,
+ "*-east": 90,
+ "*-south": 0,
+ "*-west": 270
+ }
+ },
+ "combustibleProps": {
+ "burnTemperature": 600,
+ "burnDuration": 25
+ },
+ "sounds": {
+ "place": "block/planks",
+ "break": "block/planks",
+ "hit": "block/planks"
+ },
+ "materialDensity": 400,
+ "guiTransform": {
+ "rotation": { "x": -19, "y": 136, "z": 0 },
+ "origin": { "x": 0.5, "y": 0.5, "z": 0.5 },
+ "scale": 1.9
+ },
+ "groundTransform": {
+ "translation": { "x": 0, "y": 0, "z": 0 },
+ "rotation": { "x": -90, "y": 0, "z": 0 },
+ "origin": { "x": 0.5, "y": 0, "z": 0.5 },
+ "scale": 2.2
+ },
+ "tpHandTransform": {
+ "translation": { "x": -0.45, "y": -0.8, "z": 0.04 },
+ "rotation": { "x": 0, "y": 0, "z": -10 },
+ "origin": { "x": 0.5, "y": 0.5, "z": 0.5 },
+ "scale": 0.95
+ }
+}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/assets/butterflypins/lang/en.json b/ButterflyPins/ButterflyPins/assets/butterflypins/lang/en.json
new file mode 100644
index 0000000..df6e94b
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/assets/butterflypins/lang/en.json
@@ -0,0 +1,4 @@
+{
+ "block-pinboard2x2-*": "Butterfly Pin Board (2x2)",
+ "block-pinboard3x3-*": "Butterfly Pin Board (3x3)"
+}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/assets/butterflypins/recipes/grid/pinboard2x2.json b/ButterflyPins/ButterflyPins/assets/butterflypins/recipes/grid/pinboard2x2.json
new file mode 100644
index 0000000..c902a33
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/assets/butterflypins/recipes/grid/pinboard2x2.json
@@ -0,0 +1,10 @@
+{
+ "ingredientPattern": "PPP\tS_S\tPPP",
+ "ingredients": {
+ "P": { "type": "item", "code": "plank-*" },
+ "S": { "type": "item", "code": "stick" }
+ },
+ "width": 3,
+ "height": 3,
+ "output": { "type": "block", "code": "pinboard2x2-south" }
+}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/assets/butterflypins/recipes/grid/pinboard3x3.json b/ButterflyPins/ButterflyPins/assets/butterflypins/recipes/grid/pinboard3x3.json
new file mode 100644
index 0000000..aeff790
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/assets/butterflypins/recipes/grid/pinboard3x3.json
@@ -0,0 +1,10 @@
+{
+ "ingredientPattern": "PPP\tPSP\tPPP",
+ "ingredients": {
+ "P": { "type": "item", "code": "plank-*" },
+ "S": { "type": "item", "code": "stick" }
+ },
+ "width": 3,
+ "height": 3,
+ "output": { "type": "block", "code": "pinboard3x3-south" }
+}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/assets/butterflypins/shapes/block/pinboard-2x2.json b/ButterflyPins/ButterflyPins/assets/butterflypins/shapes/block/pinboard-2x2.json
new file mode 100644
index 0000000..7b3d846
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/assets/butterflypins/shapes/block/pinboard-2x2.json
@@ -0,0 +1,131 @@
+{
+ "editor": {
+ "allAngles": false,
+ "singleTexture": false
+ },
+ "textureWidth": 16,
+ "textureHeight": 16,
+ "textures": {
+ "wood": "game:block/wood/debarked/aged",
+ "metal": "game:block/metal/tarnished/rusty-iron"
+ },
+ "elements": [
+ {
+ "name": "board",
+ "from": [1.0, 1.0, 15.0],
+ "to": [15.0, 15.0, 16.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [1.0, 1.0, 15.0, 15.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 14.0] },
+ "south": { "texture": "#wood", "uv": [1.0, 1.0, 15.0, 15.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 14.0] },
+ "up": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "down": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] }
+ }
+ },
+ {
+ "name": "lipTop",
+ "from": [1.0, 14.0, 14.5],
+ "to": [15.0, 15.0, 15.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 1.0] },
+ "south": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 1.0] },
+ "up": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 0.5] },
+ "down": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 0.5] }
+ }
+ },
+ {
+ "name": "lipBottom",
+ "from": [1.0, 1.0, 14.5],
+ "to": [15.0, 2.0, 15.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 1.0] },
+ "south": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 1.0] },
+ "up": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 0.5] },
+ "down": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 0.5] }
+ }
+ },
+ {
+ "name": "lipLeft",
+ "from": [1.0, 2.0, 14.5],
+ "to": [2.0, 14.0, 15.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 12.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 12.0] },
+ "south": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 12.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 12.0] },
+ "up": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 0.5] },
+ "down": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 0.5] }
+ }
+ },
+ {
+ "name": "lipRight",
+ "from": [14.0, 2.0, 14.5],
+ "to": [15.0, 14.0, 15.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 12.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 12.0] },
+ "south": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 12.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 12.0] },
+ "up": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 0.5] },
+ "down": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 0.5] }
+ }
+ },
+ {
+ "name": "pinTopLeft",
+ "from": [4.75, 10.75, 14.2],
+ "to": [5.25, 11.25, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinTopRight",
+ "from": [10.75, 10.75, 14.2],
+ "to": [11.25, 11.25, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinBottomLeft",
+ "from": [4.75, 4.75, 14.2],
+ "to": [5.25, 5.25, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinBottomRight",
+ "from": [10.75, 4.75, 14.2],
+ "to": [11.25, 5.25, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ButterflyPins/ButterflyPins/assets/butterflypins/shapes/block/pinboard-3x3.json b/ButterflyPins/ButterflyPins/assets/butterflypins/shapes/block/pinboard-3x3.json
new file mode 100644
index 0000000..d05fe02
--- /dev/null
+++ b/ButterflyPins/ButterflyPins/assets/butterflypins/shapes/block/pinboard-3x3.json
@@ -0,0 +1,196 @@
+{
+ "editor": {
+ "allAngles": false,
+ "singleTexture": false
+ },
+ "textureWidth": 16,
+ "textureHeight": 16,
+ "textures": {
+ "wood": "game:block/wood/debarked/aged",
+ "metal": "game:block/metal/tarnished/rusty-iron"
+ },
+ "elements": [
+ {
+ "name": "board",
+ "from": [1.0, 1.0, 15.0],
+ "to": [15.0, 15.0, 16.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [1.0, 1.0, 15.0, 15.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 14.0] },
+ "south": { "texture": "#wood", "uv": [1.0, 1.0, 15.0, 15.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 14.0] },
+ "up": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "down": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] }
+ }
+ },
+ {
+ "name": "lipTop",
+ "from": [1.0, 14.0, 14.5],
+ "to": [15.0, 15.0, 15.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 1.0] },
+ "south": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 1.0] },
+ "up": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 0.5] },
+ "down": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 0.5] }
+ }
+ },
+ {
+ "name": "lipBottom",
+ "from": [1.0, 1.0, 14.5],
+ "to": [15.0, 2.0, 15.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 1.0] },
+ "south": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 1.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 1.0] },
+ "up": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 0.5] },
+ "down": { "texture": "#wood", "uv": [1.0, 0.0, 15.0, 0.5] }
+ }
+ },
+ {
+ "name": "lipLeft",
+ "from": [1.0, 2.0, 14.5],
+ "to": [2.0, 14.0, 15.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 12.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 12.0] },
+ "south": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 12.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 12.0] },
+ "up": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 0.5] },
+ "down": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 0.5] }
+ }
+ },
+ {
+ "name": "lipRight",
+ "from": [14.0, 2.0, 14.5],
+ "to": [15.0, 14.0, 15.0],
+ "faces": {
+ "north": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 12.0] },
+ "east": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 12.0] },
+ "south": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 12.0] },
+ "west": { "texture": "#wood", "uv": [0.0, 0.0, 0.5, 12.0] },
+ "up": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 0.5] },
+ "down": { "texture": "#wood", "uv": [0.0, 0.0, 1.0, 0.5] }
+ }
+ },
+ {
+ "name": "pinA",
+ "from": [4.0, 12.0, 14.2],
+ "to": [4.5, 12.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinB",
+ "from": [8.0, 12.0, 14.2],
+ "to": [8.5, 12.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinC",
+ "from": [12.0, 12.0, 14.2],
+ "to": [12.5, 12.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinD",
+ "from": [4.0, 8.0, 14.2],
+ "to": [4.5, 8.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinE",
+ "from": [8.0, 8.0, 14.2],
+ "to": [8.5, 8.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinF",
+ "from": [12.0, 8.0, 14.2],
+ "to": [12.5, 8.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinG",
+ "from": [4.0, 4.0, 14.2],
+ "to": [4.5, 4.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinH",
+ "from": [8.0, 4.0, 14.2],
+ "to": [8.5, 4.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ },
+ {
+ "name": "pinI",
+ "from": [12.0, 4.0, 14.2],
+ "to": [12.5, 4.5, 14.8],
+ "faces": {
+ "north": { "texture": "#metal", "uv": [12.0, 11.5, 12.5, 12.0] },
+ "east": { "texture": "#metal", "uv": [10.5, 12.0, 11.0, 12.5] },
+ "south": { "texture": "#metal", "uv": [11.0, 12.5, 11.5, 13.0] },
+ "west": { "texture": "#metal", "uv": [12.0, 12.0, 12.5, 12.5] },
+ "up": { "texture": "#metal", "uv": [10.5, 12.5, 11.0, 13.0] },
+ "down": { "texture": "#metal", "uv": [10.5, 11.5, 11.0, 12.0] }
+ }
+ }
+ ]
+}
\ No newline at end of file