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
25 changes: 20 additions & 5 deletions integration-tests/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@

import initCreator from "../src/index"
import initCreator from '../src/index'
// import SampleImg from './image-sample.png'

let new_asset_id = 1000

async function test() {
const canvas = document.querySelector<HTMLCanvasElement>("canvas")!
const creator = await initCreator(canvas)
const canvas = document.querySelector<HTMLCanvasElement>('canvas')!
const creator = await initCreator(
canvas,
[
// {
// id: 1000,
// points: [
// { x: 84.5, y: 71.5, u: 0, v: 0 },
// { x: 688.5, y: 71.5, u: 1, v: 0 },
// { x: 688.5, y: 675.5, u: 1, v: 1 },
// { x: 84.5, y: 675.5, u: 0, v: 1 },
// ],
// url: SampleImg,
// },
],
console.log
)

const fileInput = document.querySelector<HTMLInputElement>('input')!
fileInput.addEventListener('change', (event) => {
const { files } = (event.target as HTMLInputElement)
const { files } = event.target as HTMLInputElement
if (!files) return

const img = new Image()
Expand Down
28 changes: 15 additions & 13 deletions src/WebGPU/pointer.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { on_pointer_move, on_pointer_click, on_pointer_down, on_pointer_up } from "../logic/index.zig"
import {
on_pointer_move,
on_pointer_click,
on_pointer_down,
on_pointer_up,
} from '../logic/index.zig'

export const pointer = { x: 0, y: 0 }


export default function initMouseController(canvas: HTMLCanvasElement) {
pointer.x = 0
pointer.y = 0

function updatePointer(e: MouseEvent) {
const rect = canvas.getBoundingClientRect()
pointer.x = e.clientX - rect.left
pointer.y = e.clientY - rect.top
}

function updatePointer(e: MouseEvent) {
const rect = canvas.getBoundingClientRect()
pointer.x = e.clientX - rect.left
pointer.y = e.clientY - rect.top
}
canvas.addEventListener('mouseleave', () => {})

canvas.addEventListener('mouseleave', () => {
})

canvas.addEventListener('mousemove', e => {
canvas.addEventListener('mousemove', (e) => {
updatePointer(e)
on_pointer_move(pointer.x, pointer.y)
})
Expand All @@ -34,7 +36,7 @@ function updatePointer(e: MouseEvent) {
on_pointer_up()
})

canvas.addEventListener("wheel", (event) => {
canvas.addEventListener('wheel', (event) => {
console.log(event.deltaY)
})
}
}
102 changes: 58 additions & 44 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,34 @@ import getDevice from 'WebGPU/getDevice'
import initPrograms from 'WebGPU/programs/initPrograms'
import runCreator from 'run'
import { createTextureFromSource } from 'WebGPU/getTexture'
import clamp from 'utils/clamp'
import { init_state, add_texture, ASSET_ID_TRESHOLD } from './logic/index.zig'
import {
init_state,
add_texture,
connectOnAssetUpdateCallback,
ASSET_ID_TRESHOLD,
} from './logic/index.zig'
import initMouseController from 'WebGPU/pointer'
import getDefaultPoints from 'utils/getDefaultPoints'

export type SerializedAsset = Omit<Texture, 'texture_id'> & {
url: string
}

export interface CreatorAPI {
addImage: (id: number, img: HTMLImageElement) => void
addImage: (id: number, img: HTMLImageElement, points?: PointUV[]) => void
destroy: VoidFunction
}

export default async function initCreator(canvas: HTMLCanvasElement): Promise<CreatorAPI> {
export interface TextureSource {
url: string
texture: GPUTexture
}

export default async function initCreator(
canvas: HTMLCanvasElement,
assets: SerializedAsset[],
onAssetsUpdate: (assets: SerializedAsset[]) => void
): Promise<CreatorAPI> {
/* setup WebGPU stuff */
const device = await getDevice()

Expand All @@ -36,52 +54,48 @@ export default async function initCreator(canvas: HTMLCanvasElement): Promise<Cr

initMouseController(canvas)

const textures: GPUTexture[] = []
const textures: TextureSource[] = []
runCreator(canvas, context, device, presentationFormat, textures)
connectOnAssetUpdateCallback((serializedData: Texture[]) => {
const serializedAssetsTextureUrl = [...serializedData].map<SerializedAsset>((asset) => ({
id: asset.id,
points: [...asset.points].map((point) => ({
x: point.x,
y: point.y,
u: point.u,
v: point.v,
})),
url: textures[asset.texture_id].url,
}))
onAssetsUpdate(serializedAssetsTextureUrl)
})
Comment on lines +59 to +71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify the deep copy of serialized data.

The spread operators and map functions create unnecessary copies. Since you're transforming the data anyway, the explicit copying is redundant.

-connectOnAssetUpdateCallback((serializedData: Texture[]) => {
-  const serializedAssetsTextureUrl = [...serializedData].map<SerializedAsset>((asset) => ({
-    id: asset.id,
-    points: [...asset.points].map((point) => ({
-      x: point.x,
-      y: point.y,
-      u: point.u,
-      v: point.v,
-    })),
-    url: textures[asset.texture_id].url,
-  }))
-  onAssetsUpdate(serializedAssetsTextureUrl)
-})
+connectOnAssetUpdateCallback((serializedData: Texture[]) => {
+  const serializedAssetsTextureUrl = serializedData.map<SerializedAsset>((asset) => ({
+    id: asset.id,
+    points: asset.points,
+    url: textures[asset.texture_id].url,
+  }))
+  onAssetsUpdate(serializedAssetsTextureUrl)
+})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
connectOnAssetUpdateCallback((serializedData: Texture[]) => {
const serializedAssetsTextureUrl = [...serializedData].map<SerializedAsset>((asset) => ({
id: asset.id,
points: [...asset.points].map((point) => ({
x: point.x,
y: point.y,
u: point.u,
v: point.v,
})),
url: textures[asset.texture_id].url,
}))
onAssetsUpdate(serializedAssetsTextureUrl)
})
connectOnAssetUpdateCallback((serializedData: Texture[]) => {
const serializedAssetsTextureUrl = serializedData.map<SerializedAsset>((asset) => ({
id: asset.id,
points: asset.points,
url: textures[asset.texture_id].url,
}))
onAssetsUpdate(serializedAssetsTextureUrl)
})
🤖 Prompt for AI Agents
In src/index.ts around lines 59 to 71, the current code uses spread operators
and map functions to create deep copies of serializedData and its nested points,
which is redundant because you are already transforming the data. Simplify the
code by directly mapping over serializedData and points without spreading them
into new arrays, as the transformation itself produces new objects and no
explicit deep copy is needed.


return {
addImage: (id, img) => {
if (id < ASSET_ID_TRESHOLD) {
throw Error(`ID should be unique and not smaller than ${ASSET_ID_TRESHOLD}.`)
}
const newTextureIndex = textures.length
textures.push(createTextureFromSource(device, img))
const scale = getDefaultTextureScale(img, canvas)
const scaledWidth = img.width * scale
const scaledHeight = img.height * scale
const paddingX = (canvas.width - scaledWidth) * 0.5
const paddingY = (canvas.height - scaledHeight) * 0.5
function addImage(id: number, img: HTMLImageElement, points?: PointUV[]) {
if (id < ASSET_ID_TRESHOLD) {
throw Error(`ID should be unique and not smaller than ${ASSET_ID_TRESHOLD}.`)
}
const newTextureIndex = textures.length
textures.push({
url: img.src,
texture: createTextureFromSource(device, img),
})

add_texture(
id,
[
{ x: paddingX, y: paddingY, u: 0, v: 0 },
{ x: paddingX + scaledWidth, y: paddingY, u: 1, v: 0 },
{ x: paddingX + scaledWidth, y: paddingY + scaledHeight, u: 1, v: 1 },
{ x: paddingX, y: paddingY + scaledHeight, u: 0, v: 1 },
],
newTextureIndex
)
},
add_texture(id, points || getDefaultPoints(img, canvas), newTextureIndex)
}

assets.forEach((asset) => {
const img = new Image()
img.src = asset.url
img.onload = () => {
addImage(asset.id, img, asset.points)
}
})
Comment on lines +86 to +92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for image loading failures.

If an image fails to load, the error is silently ignored, which could lead to missing assets without any indication to the user.

 assets.forEach((asset) => {
   const img = new Image()
   img.src = asset.url
   img.onload = () => {
     addImage(asset.id, img, asset.points)
   }
+  img.onerror = (error) => {
+    console.error(`Failed to load asset ${asset.id} from ${asset.url}:`, error)
+  }
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assets.forEach((asset) => {
const img = new Image()
img.src = asset.url
img.onload = () => {
addImage(asset.id, img, asset.points)
}
})
assets.forEach((asset) => {
const img = new Image()
img.src = asset.url
img.onload = () => {
addImage(asset.id, img, asset.points)
}
img.onerror = (error) => {
console.error(`Failed to load asset ${asset.id} from ${asset.url}:`, error)
}
})
🤖 Prompt for AI Agents
In src/index.ts around lines 86 to 92, the image loading process lacks error
handling for failures. Add an onerror event handler to the Image object that
logs or handles the error appropriately when an image fails to load, ensuring
that loading failures are detected and reported instead of being silently
ignored.


return {
addImage,
destroy: () => {
context.unconfigure()
device.destroy()
},
}
}

/**
* Returns visualy pleasant size of texture, to make sure it doesn't overflow canvas but also is not too small to manipulate
*/
function getDefaultTextureScale(img: HTMLImageElement, canvas: HTMLCanvasElement) {
const heightDiff = canvas.height - img.height
const widthDiff = canvas.width - img.width

if (heightDiff < widthDiff) {
const height = clamp(img.height, canvas.height * 0.2, canvas.height * 0.8)
return height / img.height
}

const width = clamp(img.width, canvas.width * 0.2, canvas.width * 0.8)
return width / img.width
}
11 changes: 9 additions & 2 deletions src/logic/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ interface PointUV {
}

type ZigF32Array = { typedArray: Float32Array }
type Texture = {
id: number
points: PointUV[]
texture_id: number
}

declare module "*.zig" {
declare module '*.zig' {
export const ASSET_ID_TRESHOLD: number
export const init_state: (width: number, height: number) => void
export const add_texture: (id: number, points: PointUV[], textre_index: number) => void
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the typo in the parameter name.

The parameter textre_index should be texture_index.

-export const add_texture: (id: number, points: PointUV[], textre_index: number) => void
+export const add_texture: (id: number, points: PointUV[], texture_index: number) => void
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const add_texture: (id: number, points: PointUV[], textre_index: number) => void
export const add_texture: (id: number, points: PointUV[], texture_index: number) => void
🤖 Prompt for AI Agents
In src/logic/index.d.ts at line 18, there is a typo in the parameter name
`textre_index`. Rename this parameter to `texture_index` to correct the
spelling.

Expand All @@ -24,6 +29,8 @@ declare module "*.zig" {
draw_triangle: (vertexData: ZigF32Array) => void
pick_texture: (vertexData: ZigF32Array, texture_id: number) => void
}) => void
export const connectOnAssetUpdateCallback: (cb: (data: Texture[]) => void) => void

export const canvas_render: VoidFunction
export const picks_render: VoidFunction
}
}
17 changes: 17 additions & 0 deletions src/logic/index.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ pub fn connectWebGPUPrograms(programs: *const WebGpuPrograms) void {
web_gpu_programs = programs; // orelse WebGpuPrograms{};
}

var on_asset_update_cb: *const fn ([]Texture) void = undefined;
pub fn connectOnAssetUpdateCallback(cb: *const fn ([]Texture) void) void {
on_asset_update_cb = cb;
}
Comment on lines +21 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Initialize the callback pointer to prevent undefined behavior.

The on_asset_update_cb is declared as undefined, which could cause a crash if on_pointer_up is called before connectOnAssetUpdateCallback.

Consider initializing with a no-op function or checking for undefined before calling:

-var on_asset_update_cb: *const fn ([]Texture) void = undefined;
+var on_asset_update_cb: ?*const fn ([]Texture) void = null;

Then update the invocation to check for null:

-if (result.len > 0) {
-    on_asset_update_cb(result);
-}
+if (result.len > 0 and on_asset_update_cb != null) {
+    on_asset_update_cb.?(result);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var on_asset_update_cb: *const fn ([]Texture) void = undefined;
pub fn connectOnAssetUpdateCallback(cb: *const fn ([]Texture) void) void {
on_asset_update_cb = cb;
}
// src/logic/index.zig
// Change the callback pointer to an optional initialized to null
-var on_asset_update_cb: *const fn ([]Texture) void = undefined;
+var on_asset_update_cb: ?*const fn ([]Texture) void = null;
pub fn connectOnAssetUpdateCallback(cb: *const fn ([]Texture) void) void {
on_asset_update_cb = cb;
}
// …
// In on_pointer_up (around lines 86–97), guard against a null callback before calling
- if (result.len > 0) {
- on_asset_update_cb(result);
- }
+ if (result.len > 0 and on_asset_update_cb != null) {
+ on_asset_update_cb.?(result);
+ }
🤖 Prompt for AI Agents
In src/logic/index.zig around lines 21 to 24, the callback pointer
on_asset_update_cb is initialized as undefined, which risks undefined behavior
if called before being set. Fix this by initializing on_asset_update_cb to a
no-op function that matches its signature or by adding a check before invoking
it to ensure it is not undefined. This prevents crashes by ensuring the callback
pointer is always valid or safely skipped.


pub const ASSET_ID_TRESHOLD: u32 = 1000;

const ActionType = enum {
Expand Down Expand Up @@ -78,6 +83,18 @@ pub fn on_pointer_down(x: f32, y: f32) void {

pub fn on_pointer_up() void {
state.ongoing_action = .none;

var result = std.heap.page_allocator.alloc(Texture, state.assets.count()) catch unreachable;
var iterator = state.assets.iterator();
var i: usize = 0;
while (iterator.next()) |entry| {
result[i] = entry.value_ptr.*;
i += 1;
}

if (result.len > 0) {
on_asset_update_cb(result);
}
Comment on lines +87 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix memory leak - allocated array is never freed.

The array allocated with std.heap.page_allocator.alloc is never deallocated, causing a memory leak on every pointer up event.

Since the callback likely needs to process the data asynchronously, consider either:

  1. Documenting that the callback is responsible for freeing the memory
  2. Using a different allocation strategy (e.g., arena allocator)
  3. Keeping a persistent buffer that gets reused

For immediate fix, if the callback processes synchronously, add deallocation:

 if (result.len > 0) {
     on_asset_update_cb(result);
 }
+defer std.heap.page_allocator.free(result);

However, this won't work if the callback stores the reference. Consider passing ownership to the JavaScript side or using a different memory management approach.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var result = std.heap.page_allocator.alloc(Texture, state.assets.count()) catch unreachable;
var iterator = state.assets.iterator();
var i: usize = 0;
while (iterator.next()) |entry| {
result[i] = entry.value_ptr.*;
i += 1;
}
if (result.len > 0) {
on_asset_update_cb(result);
}
var result = std.heap.page_allocator.alloc(Texture, state.assets.count()) catch unreachable;
var iterator = state.assets.iterator();
var i: usize = 0;
while (iterator.next()) |entry| {
result[i] = entry.value_ptr.*;
i += 1;
}
if (result.len > 0) {
on_asset_update_cb(result);
}
defer std.heap.page_allocator.free(result);
🤖 Prompt for AI Agents
In src/logic/index.zig around lines 87 to 97, the array allocated with
std.heap.page_allocator.alloc is not freed, causing a memory leak. To fix this,
if the on_asset_update_cb callback processes the data synchronously and does not
retain the reference, add a call to free the allocated array immediately after
the callback returns. If the callback stores the reference asynchronously,
update the design to either transfer ownership to the caller or use a persistent
or arena allocator to manage the buffer lifecycle properly.

}

pub fn on_pointer_move(x: f32, y: f32) void {
Expand Down
10 changes: 5 additions & 5 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { drawTexture, drawTriangle, pickTexture } from 'WebGPU/programs/initProg
import getCanvasMatrix from 'getCanvasMatrix'
import PickManager from 'WebGPU/pick'
import { canvas_render, picks_render, connectWebGPUPrograms } from 'logic/index.zig'
import { TextureSource } from '.'

export const transformMatrix = new Float32Array()
export const MAP_BACKGROUND_SCALE = 1000
Expand All @@ -12,7 +13,7 @@ export default function runCreator(
context: GPUCanvasContext,
device: GPUDevice,
presentationFormat: GPUTextureFormat,
textures: GPUTexture[]
textures: TextureSource[]
) {
const canvasMatrix = getCanvasMatrix(canvas)
let canvasPass: GPURenderPassEncoder
Expand All @@ -22,12 +23,11 @@ export default function runCreator(
let pickPass: GPURenderPassEncoder

connectWebGPUPrograms({
draw_texture: (vertex_data, texture_id) => {
drawTexture(canvasPass, canvasMatrix, vertex_data.typedArray, textures[texture_id])
},
draw_texture: (vertex_data, texture_id) =>
drawTexture(canvasPass, canvasMatrix, vertex_data.typedArray, textures[texture_id].texture),
draw_triangle: (vertex_data) => drawTriangle(canvasPass, canvasMatrix, vertex_data.typedArray),
pick_texture: (vertex_data, texture_id) =>
pickTexture(pickPass, pickMatrix, vertex_data.typedArray, textures[texture_id]),
pickTexture(pickPass, pickMatrix, vertex_data.typedArray, textures[texture_id].texture),
Comment on lines +26 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding bounds checking for texture array access.

The code accesses textures[texture_id] without bounds checking. If texture_id is out of range, this will cause a runtime error.

Consider adding bounds validation:

  connectWebGPUPrograms({
    draw_texture: (vertex_data, texture_id) => {
+     if (texture_id >= textures.length) {
+       console.error(`Invalid texture_id: ${texture_id}`)
+       return
+     }
      drawTexture(canvasPass, canvasMatrix, vertex_data.typedArray, textures[texture_id].texture)
    },
    pick_texture: (vertex_data, texture_id) => {
+     if (texture_id >= textures.length) {
+       console.error(`Invalid texture_id: ${texture_id}`)
+       return
+     }
      pickTexture(pickPass, pickMatrix, vertex_data.typedArray, textures[texture_id].texture)
    },
  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
draw_texture: (vertex_data, texture_id) =>
drawTexture(canvasPass, canvasMatrix, vertex_data.typedArray, textures[texture_id].texture),
draw_triangle: (vertex_data) => drawTriangle(canvasPass, canvasMatrix, vertex_data.typedArray),
pick_texture: (vertex_data, texture_id) =>
pickTexture(pickPass, pickMatrix, vertex_data.typedArray, textures[texture_id]),
pickTexture(pickPass, pickMatrix, vertex_data.typedArray, textures[texture_id].texture),
connectWebGPUPrograms({
draw_texture: (vertex_data, texture_id) => {
if (texture_id < 0 || texture_id >= textures.length) {
console.error(`Invalid texture_id: ${texture_id}`)
return
}
drawTexture(
canvasPass,
canvasMatrix,
vertex_data.typedArray,
textures[texture_id].texture
)
},
draw_triangle: (vertex_data) =>
drawTriangle(canvasPass, canvasMatrix, vertex_data.typedArray),
pick_texture: (vertex_data, texture_id) => {
if (texture_id < 0 || texture_id >= textures.length) {
console.error(`Invalid texture_id: ${texture_id}`)
return
}
pickTexture(
pickPass,
pickMatrix,
vertex_data.typedArray,
textures[texture_id].texture
)
},
})
🤖 Prompt for AI Agents
In src/run.ts around lines 26 to 30, the code accesses textures[texture_id]
without checking if texture_id is within the valid range, which can cause
runtime errors. Add bounds checking before accessing textures[texture_id] to
ensure texture_id is a valid index within the textures array. If texture_id is
invalid, handle the case gracefully, such as by returning early, throwing a
descriptive error, or using a default texture.

})

function draw(now: DOMHighResTimeStamp) {
Expand Down
35 changes: 35 additions & 0 deletions src/utils/getDefaultPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import clamp from 'utils/clamp'

export default function getDefaultPoints(
img: HTMLImageElement,
canvas: HTMLCanvasElement
): PointUV[] {
const scale = getDefaultTextureScale(img, canvas)
const scaledWidth = img.width * scale
const scaledHeight = img.height * scale
const paddingX = (canvas.width - scaledWidth) * 0.5
const paddingY = (canvas.height - scaledHeight) * 0.5

return [
{ x: paddingX, y: paddingY, u: 0, v: 0 },
{ x: paddingX + scaledWidth, y: paddingY, u: 1, v: 0 },
{ x: paddingX + scaledWidth, y: paddingY + scaledHeight, u: 1, v: 1 },
{ x: paddingX, y: paddingY + scaledHeight, u: 0, v: 1 },
]
}

/**
* Returns visualy pleasant size of texture, to make sure it doesn't overflow canvas but also is not too small to manipulate
*/
function getDefaultTextureScale(img: HTMLImageElement, canvas: HTMLCanvasElement) {
const heightDiff = canvas.height - img.height
const widthDiff = canvas.width - img.width

if (heightDiff < widthDiff) {
const height = clamp(img.height, canvas.height * 0.2, canvas.height * 0.8)
return height / img.height
}

const width = clamp(img.width, canvas.width * 0.2, canvas.width * 0.8)
return width / img.width
}