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
6 changes: 2 additions & 4 deletions src/WebGPU/getTexture/createCheckedImageData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

const FAKE_MIPMAPS_COLORS = [
'#FF0000',
'#FF00C4',
Expand All @@ -13,7 +12,7 @@ const FAKE_MIPMAPS_COLORS = [
'#FFBC00',
]

const ctx = document.createElement('canvas').getContext('2d', {willReadFrequently: true})!
const ctx = new OffscreenCanvas(0, 0).getContext('2d', { willReadFrequently: true })!

export default function createCheckedImageData(size: number, index: number): ImageData {
ctx.canvas.width = size
Expand All @@ -33,10 +32,9 @@ export default function createCheckedImageData(size: number, index: number): Ima
{ x: 0.25, y: 0.75 },
{ x: 0.75, y: 0.75 },
{ x: 0.75, y: 0.25 },
].forEach(p => {
].forEach((p) => {
ctx.fillText(index.toString(), p.x * size, p.y * size)
})


return ctx.getImageData(0, 0, size, size)
}
75 changes: 46 additions & 29 deletions src/WebGPU/getTexture/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import createCheckedImageData from "./createCheckedImageData"
import generateMipmapsArray from "./generateMimapsArray"
import createCheckedImageData from './createCheckedImageData'
import generateMipmapsArray from './generateMimapsArray'

interface Options {
mips?: boolean
Expand All @@ -8,26 +8,26 @@ interface Options {
}

type TextureSource =
| ImageBitmap
| HTMLVideoElement
| HTMLCanvasElement
| HTMLImageElement
| OffscreenCanvas;
| ImageBitmap
| HTMLVideoElement
| HTMLCanvasElement
| HTMLImageElement
| OffscreenCanvas

const numMipLevels = (...sizes: number[]) => {
const maxSize = Math.max(...sizes)
return 1 + Math.log2(maxSize) | 0
return (1 + Math.log2(maxSize)) | 0
}

export interface TextureSlice {
source: GPUCopyExternalImageSource
fakeMipmaps: boolean
}

function createCheckedMipmap(levels: Array<{ size: number, color: string }>) {
const ctx = document.createElement('canvas').getContext('2d', {willReadFrequently: true})!
function createCheckedMipmap(levels: Array<{ size: number; color: string }>) {
const ctx = new OffscreenCanvas(0, 0).getContext('2d', { willReadFrequently: true })!

return levels.map(({size, color}, i) => {
return levels.map(({ size, color }, i) => {
ctx.canvas.width = size
ctx.canvas.height = size
ctx.fillStyle = i & 1 ? '#000' : '#fff'
Expand All @@ -45,24 +45,29 @@ function createCheckedMipmap(levels: Array<{ size: number, color: string }>) {
{ x: 0.25, y: 0.75 },
{ x: 0.75, y: 0.75 },
{ x: 0.75, y: 0.25 },
].forEach(p => {
].forEach((p) => {
ctx.fillText(i.toString(), p.x * size, p.y * size)
})


return ctx.getImageData(0, 0, size, size)
})
}

export function createTextureArray(device: GPUDevice, width: number, height: number, slices: number) {
export function createTextureArray(
device: GPUDevice,
width: number,
height: number,
slices: number
) {
return device.createTexture({
label: '2d array texture',
format: 'rgba8unorm',
mipLevelCount: 1 + Math.log2(2048),
size: [width, height, slices],
usage: GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
})
}

Expand All @@ -79,7 +84,7 @@ export function attachSlice(
device.queue.copyExternalImageToTexture(
{ source },
{ texture: textue2dArray, origin: { z: sliceIndex }, mipLevel: 0 },
{ width, height },
{ width, height }
)

// if (texSlice.fakeMipmaps) {
Expand All @@ -102,28 +107,37 @@ export function attachSlice(
// }
}


export function createTextureFromSource(device: GPUDevice, source: TextureSource, options: Options = {}) {
export function createTextureFromSource(
device: GPUDevice,
source: TextureSource,
options: Options = {}
) {
const texture = device.createTexture({
format: 'rgba8unorm',
mipLevelCount: options.mips ? numMipLevels(source.width, source.height) : 1,
size: [source.width, source.height],
usage: GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
})
copySourceToTexture(device, texture, source, options)
return texture
}

function copySourceToTexture(device: GPUDevice, texture: GPUTexture, source: TextureSource, {flipY, depthOrArrayLayers}: Options = {}) {

function copySourceToTexture(
device: GPUDevice,
texture: GPUTexture,
source: TextureSource,
{ flipY, depthOrArrayLayers }: Options = {}
) {
device.queue.copyExternalImageToTexture(
{ source, flipY, },
{ texture,
{ source, flipY },
{
texture,
// premultipliedAlpha: true
},
{ width: source.width, height: source.height, depthOrArrayLayers },
{ width: source.width, height: source.height, depthOrArrayLayers }
)

// if (texture.mipLevelCount > 1) {
Expand All @@ -134,7 +148,10 @@ function copySourceToTexture(device: GPUDevice, texture: GPUTexture, source: Tex
export async function loadImageBitmap(url: string) {
const res = await fetch(url)
const blob = await res.blob()
return await createImageBitmap(blob, { colorSpaceConversion: 'none', premultiplyAlpha: 'premultiply' })
return await createImageBitmap(blob, {
colorSpaceConversion: 'none',
premultiplyAlpha: 'premultiply',
})
}

export async function createTextureFromImage(device: GPUDevice, url: string, options: Options) {
Expand Down
12 changes: 6 additions & 6 deletions src/WebGPU/programs/drawShape/getProgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ export default function getDrawShape(
const uniformBufferSize =
(1 /*stroke width*/ + 4 /*stroke color*/ + 4 /*fill color*/ + /*padding*/ 3) * 4

const uniformBuffer = device.createBuffer({
label: 'drawShape uniforms',
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})

const bindGroupLayout = device.createBindGroupLayout({
label: 'drawShape bind group layout',
entries: [
Expand Down Expand Up @@ -108,7 +102,13 @@ export default function getDrawShape(
device.queue.writeBuffer(curvesBuffer, 0, curvesDataView)
buffersToDestroy.push(curvesBuffer)

const uniformBuffer = device.createBuffer({
label: 'drawShape uniforms',
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})
device.queue.writeBuffer(uniformBuffer, 0, uniformDataView)
buffersToDestroy.push(uniformBuffer)

passEncoder.setPipeline(renderPipeline)

Expand Down
22 changes: 14 additions & 8 deletions src/WebGPU/programs/drawShape/shader.wgsl
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const STRAIGHT_LINE_THRESHOLD = 1e10;
const EPSILON = 1e-10;

struct Uniforms {
stroke_width: f32,
stroke_color: vec4f,
fill_color: vec4f,
stroke_color: vec4f,
};

struct CubicBezier {
Expand Down Expand Up @@ -305,12 +306,14 @@ fn evaluate_shape(point: vec2f) -> ShapeInfo {

// Check if this is a straight line (handle points have x >= STRAIGHT_LINE_THRESHOLD)
let is_straight_line = curve.p1.x > STRAIGHT_LINE_THRESHOLD && curve.p2.x > STRAIGHT_LINE_THRESHOLD;
var distance: f32;

var distance: f32 = 1e+10;

if (is_straight_line) {
// Handle as straight line from p0 to p3
distance = distance_to_line_segment(point, curve.p0, curve.p3);
if (u.stroke_width > EPSILON) {
distance = distance_to_line_segment(point, curve.p0, curve.p3);
}

// Simple ray casting for line segment
if (ray_crosses_segment(point, curve.p0, curve.p3)) {
Expand All @@ -325,9 +328,12 @@ fn evaluate_shape(point: vec2f) -> ShapeInfo {
}
} else {
// Handle as normal cubic Bézier curve
let closest_t = closest_point_on_bezier(point, curve);
let closest_point = bezier_point(curve, closest_t);
distance = length(point - closest_point);

if (u.stroke_width > EPSILON) {
let closest_t = closest_point_on_bezier(point, curve);
let closest_point = bezier_point(curve, closest_t);
distance = length(point - closest_point);
}

// Ray casting for curve
total_crossings += ray_cast_curve_crossing(point, curve);
Expand Down
11 changes: 10 additions & 1 deletion src/logic/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ interface BoundingBox {
max_y: number
}

type ShapeProps = Partial<{
fill_color: [number, number, number, number]
stroke_color: [number, number, number, number]
stroke_width: number
}>

type ZigF32Array = { typedArray: Float32Array }
type ZigAssetInput = {
id: number
Expand Down Expand Up @@ -80,5 +86,8 @@ declare module '*.zig' {

export const import_icons: (data: number[]) => void

export const add_shape: (lines: Array<Array<[Point, Point, Point, Point]>>) => void
export const add_shape: (
paths: Array<Array<[Point, Point, Point, Point]>>,
props: ShapeProps
) => void
}
4 changes: 2 additions & 2 deletions src/logic/index.zig
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,9 @@ pub fn stop_drawing_shape() void {
state.selected_asset_id = 0;
}

pub fn add_shape(paths: []const []const [4]Types.Point) !void {
pub fn add_shape(paths: []const []const [4]Types.Point, props: shapes.ShapeProps) !void {
const id = generate_id();
const shape = try shapes.Shape.new_from_points(id, paths, std.heap.page_allocator);
const shape = try shapes.Shape.new_from_points(id, paths, props, std.heap.page_allocator);
try state.assets.put(id, Asset{ .shape = shape });
state.selected_asset_id = id;
}
Expand Down
23 changes: 15 additions & 8 deletions src/logic/shapes/shapes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ fn getOppositeHandle(control_point: Point, handle: Point) Point {
pub var cache_shape: *const fn (?u32, bounding_box.BoundingBox, DrawVertexOutput, f32, f32) u32 = undefined;
pub var max_texture_size: f32 = 0.0;

pub const ShapeProps = struct {
// f32 instead of u8 because Uniforms in wgsl doesn't support u8 anyway
fill_color: [4]f32 = .{ 0.0, 0.0, 0.0, 1.0 }, // Default fill color (red)
stroke_color: [4]f32 = .{ 0.0, 0.0, 0.0, 1.0 }, // Default stroke color (green)
stroke_width: f32 = 0.0, // Default stroke width
};

pub const Shape = struct {
id: u32,
paths: std.ArrayList(Path),
stroke_width: f32,
props: ShapeProps,
preview_point: ?Point = null, // Optional preview points for rendering
is_handle_preview: bool = false, // Whether to show the preview point as a handle
active_path_index: ?usize = null, // Index of the active path for editing
Expand All @@ -46,7 +53,7 @@ pub const Shape = struct {
const shape = Shape{
.id = id,
.paths = paths_list,
.stroke_width = 10.0,
.props = ShapeProps{},
.is_handle_preview = true,
.active_path_index = 0,
};
Expand All @@ -57,7 +64,7 @@ pub const Shape = struct {
// Arrays: Use &array to get a slice reference
// Slices: Pass directly (they're already slices)
// ArrayList: Use .items to get the underlying slice
pub fn new_from_points(id: u32, input_paths: []const []const [4]Point, allocator: std.mem.Allocator) !Shape {
pub fn new_from_points(id: u32, input_paths: []const []const [4]Point, props: ShapeProps, allocator: std.mem.Allocator) !Shape {
var paths_list = std.ArrayList(Path).init(allocator);

for (input_paths) |input_path| {
Expand All @@ -68,7 +75,7 @@ pub const Shape = struct {
const shape = Shape{
.id = id,
.paths = paths_list,
.stroke_width = 10.0,
.props = props,
.is_handle_preview = false,
.active_path_index = 0,
};
Expand Down Expand Up @@ -212,7 +219,7 @@ pub const Shape = struct {
if (curves_buffer.items.len > 0) {
// Shape.prepare_half_straight_lines(curves);

const box = bounding_box.getBoundingBox(curves_buffer.items, self.stroke_width / 2.0);
const box = bounding_box.getBoundingBox(curves_buffer.items, self.props.stroke_width / 2.0);
const box_vertex = [6]Point{
// First triangle
.{ .x = box.min_x, .y = box.min_y }, // bottom-left
Expand All @@ -229,9 +236,9 @@ pub const Shape = struct {
.curves = curves_buffer.items, // Transfer ownership directly
.bounding_box = box_vertex,
.uniform = Uniform{
.stroke_width = self.stroke_width,
.fill_color = [4]f32{ 1.0, 0.0, 0.0, 1.0 },
.stroke_color = [4]f32{ 0.0, 1.0, 0.0, 1.0 },
.stroke_width = self.props.stroke_width,
.fill_color = self.props.fill_color,
.stroke_color = self.props.stroke_color,
},
};
} else {
Expand Down
File renamed without changes.
Loading