Skip to content

Create a Plugin

This example creates a TNT plugin that demonstrates custom blocks, commands, systems, and keybindings. Place TNT and rocks on the canvas, then press Space or click the Explode button to blow up the TNT and any nearby rocks.

<script setup lang="ts">
import { WovenCanvas, SelectTool, HandTool, Toolbar } from '@woven-canvas/vue'
import '@woven-canvas/vue/style.css'
import { TntPlugin, Tnt, Rock } from './TntPlugin'
import TntBlock from './TntBlock.vue'
import RockBlock from './RockBlock.vue'
import TntTool from './TntTool.vue'
import RockTool from './RockTool.vue'
import ExplodeButton from './ExplodeButton.vue'
// Block definitions for TNT and Rock
const blockDefs = [
{
tag: 'tnt',
components: [Tnt],
},
{
tag: 'rock',
components: [Rock],
},
]
</script>
<template>
<WovenCanvas :editor="{ plugins: [TntPlugin], blockDefs }">
<!-- Toolbar with placement tools -->
<template #toolbar>
<Toolbar>
<SelectTool />
<HandTool />
<TntTool />
<RockTool />
</Toolbar>
</template>
<!-- Block renderers -->
<template #block:tnt="props">
<TntBlock v-bind="props" />
</template>
<template #block:rock="props">
<RockBlock v-bind="props" />
</template>
<!-- External UI: Explode button -->
<template #default>
<div class="ui-overlay">
<ExplodeButton />
<div class="hint">Press Space to explode</div>
</div>
</template>
</WovenCanvas>
</template>
<style scoped>
.ui-overlay {
position: absolute;
top: 16px;
right: 16px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
z-index: 100;
pointer-events: auto;
}
.hint {
font-size: 12px;
color: var(--wov-gray-400);
background: var(--wov-gray-800);
padding: 4px 8px;
border-radius: 4px;
}
</style>

Defines the complete plugin with:

  • Components: Tnt and Rock marker components to identify block types
  • Command: Explode command that can be spawned from UI or keybinds
  • System: explodeSystem runs in the update phase, queries for TNT blocks, finds nearby rocks using intersectAabb, and deletes them
  • Keybind: Binds Space to the “explode” command

The system demonstrates spatial queries using intersectAabb to find all rocks within a blast radius around each TNT block.

TntBlock.vue & RockBlock.vue — Block Renderers

Section titled “TntBlock.vue & RockBlock.vue — Block Renderers”

Simple block renderers that display TNT and rocks. TntBlock.vue uses useComponent() to reactively read block and TNT data (including blast radius), while RockBlock.vue is a lightweight visual renderer.

TntTool.vue & RockTool.vue — Toolbar Buttons

Section titled “TntTool.vue & RockTool.vue — Toolbar Buttons”

Toolbar buttons that use placement-snapshot to create blocks with the appropriate tag and components when clicked or dragged.

A button outside the canvas that:

  • Uses useQuery to track all TNT blocks on the canvas
  • Disables itself when no TNT exists
  • Spawns the Explode command via editor.command() when clicked

Registers the plugin with the editor:

  • Passes TntPlugin to the editor’s plugins option
  • Sets up block definitions with tags and components
  • Renders the toolbar, block slots, and external UI

Commands are ephemeral entities that exist for one frame. External code spawns them, and systems react to them:

import { defineCommand, on } from '@woven-canvas/core'
// Define a command
const Explode = defineCommand<void>('explode')
// Spawn from UI
editor.command(Explode)
// React in a system using on()
on(ctx, Explode, (ctx) => {
// Handle explosion
})

Keybinds map key combinations to commands:

import { Key } from '@woven-canvas/core'
const keybinds = [
{ command: 'explode', key: Key.Space }
]

Define data that gets synced with the document using defineCanvasComponent:

import { field } from '@woven-canvas/core'
import { defineCanvasComponent } from '@woven-canvas/vue'
export const Tnt = defineCanvasComponent(
{ name: 'tnt', sync: 'document' },
{
blastRadius: field.float64().default(100),
},
)

Systems run every frame in a specific phase. Use on() to react to commands:

import { defineEditorSystem, on, type Context } from '@woven-canvas/core'
const explodeSystem = defineEditorSystem(
{ phase: 'update' },
(ctx: Context) => {
on(ctx, Explode, handleExplode)
}
)

Queries efficiently find entities with specific components:

import { defineQuery, Block } from '@woven-canvas/core'
const tntQuery = defineQuery((q) => q.with(Block, Tnt))
// In a system
for (const entityId of tntQuery.current(ctx)) {
const tnt = Tnt.read(ctx, entityId)
}

Find entities within a bounding box using intersectAabb:

import { intersectAabb } from '@woven-canvas/core'
import type { Aabb } from '@woven-canvas/math'
const blastBounds: Aabb = [
center[0] - radius, center[1] - radius,
center[0] + radius, center[1] + radius
]
const nearby = intersectAabb(ctx, blastBounds, rockEntities)

Reactively read ECS component data in Vue:

import { useComponent } from '@woven-canvas/vue'
const tnt = useComponent(props.entityId, Tnt)
// Access reactively
const radius = tnt.value?.blastRadius