diff --git a/src/components/ViewportAnnotationOverlay.tsx b/src/components/ViewportAnnotationOverlay.tsx
index 9767e1c91b9..6d3a09825e5 100644
--- a/src/components/ViewportAnnotationOverlay.tsx
+++ b/src/components/ViewportAnnotationOverlay.tsx
@@ -1,5 +1,13 @@
-import { TRIM_PREVIEW_LINE_COLOR } from '@src/lib/constants'
-import { getTrimPreviewLineWidth } from '@src/lib/freehandLineDrawing'
+import { useSignals } from '@preact/signals-react/runtime'
+import {
+ ZOODLE_BRUSH_SIZE_MAX_PX,
+ ZOODLE_BRUSH_SIZE_MIN_PX,
+ type ZoodleDrawToolDefinition,
+ type ZoodleService,
+ type ZoodleToolDefinition,
+ type ZoodleToolKey,
+ zoodleToolKeys,
+} from '@src/registry/contracts/zoodle'
import type { PointerEvent as ReactPointerEvent, SyntheticEvent } from 'react'
import { useEffect, useRef, useState } from 'react'
@@ -45,17 +53,159 @@ const stopOverlayEventPropagation = (event: SyntheticEvent) => {
event.stopPropagation()
}
+const getDrawToolStrokeStyle = (
+ tool: ZoodleDrawToolDefinition,
+ canvas: HTMLCanvasElement
+) => {
+ if (tool.color === 'currentColor') {
+ return getComputedStyle(canvas).color
+ }
+
+ return tool.color
+}
+
+const getDrawToolColorClassName = (tool: ZoodleDrawToolDefinition) =>
+ tool.colorClassName || ''
+
+const toolButtonClassName = (tool: ZoodleToolDefinition, isActive: boolean) =>
+ `m-0 flex h-7 items-center justify-center rounded-sm border p-0 text-xs ${
+ tool.type === 'erase' ? 'w-auto px-2' : 'w-7'
+ } ${
+ isActive
+ ? 'border-chalkboard-50 bg-chalkboard-20 dark:border-chalkboard-60 dark:bg-chalkboard-80'
+ : 'border-transparent bg-transparent hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80'
+ }`
+
+const toolbarButtonClassName =
+ 'm-0 h-7 whitespace-nowrap rounded-sm border-none bg-transparent px-2 text-xs hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80'
+
+interface ZoodleToolbarProps {
+ activeToolKey: ZoodleToolKey
+ activeTool: ZoodleToolDefinition
+ brushSize: number
+ disabledSend: boolean
+ onCancel: () => void
+ onClear: () => void
+ onSend: () => void
+ zoodle: ZoodleService
+}
+
+const ZoodleToolbar = (props: ZoodleToolbarProps) => (
+
+
+
+ {zoodleToolKeys.map((toolKey: ZoodleToolKey) => {
+ const tool = props.zoodle.toolDefinitions[toolKey]
+ const isActive = props.activeToolKey === toolKey
+
+ return (
+
+ )
+ })}
+
+
+
+
+
+ {
+ props.zoodle.setBrushSize(Number(event.target.value))
+ }}
+ />
+
+
+
+
+
+
+
+
+)
+
interface ViewportAnnotationOverlayProps {
imageDataUrl: string
onCancel: () => void
onSend: (annotatedDataUrl: string) => void
+ zoodle: ZoodleService
}
export const ViewportAnnotationOverlay = (
props: ViewportAnnotationOverlayProps
) => {
+ useSignals()
const canvasRef = useRef
(null)
const lastPoint = useRef<{ x: number; y: number } | null>(null)
+ const activeToolKey = props.zoodle.activeToolKey.value
+ const activeTool = props.zoodle.toolDefinitions[activeToolKey]
+ const brushSize = props.zoodle.brushSize.value
const [imageSize, setImageSize] = useState<{
width: number
height: number
@@ -105,8 +255,15 @@ export const ViewportAnnotationOverlay = (
const nextPoint = getCanvasPoint(canvas, event)
const rect = canvas.getBoundingClientRect()
const pixelRatio = rect.width > 0 ? canvas.width / rect.width : 1
- ctx.strokeStyle = TRIM_PREVIEW_LINE_COLOR
- ctx.lineWidth = getTrimPreviewLineWidth(pixelRatio)
+ ctx.globalCompositeOperation =
+ activeTool.type === 'erase' ? 'destination-out' : 'source-over'
+ ctx.strokeStyle =
+ activeTool.type === 'draw'
+ ? getDrawToolStrokeStyle(activeTool, canvas)
+ : '#000000'
+ const lineWidthMultiplier =
+ 'lineWidthMultiplier' in activeTool ? activeTool.lineWidthMultiplier : 1
+ ctx.lineWidth = brushSize * pixelRatio * lineWidthMultiplier
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
ctx.beginPath()
@@ -184,11 +341,14 @@ export const ViewportAnnotationOverlay = (
})
}
+ const activeDrawToolClassName =
+ activeTool.type === 'draw' ? getDrawToolColorClassName(activeTool) : ''
+
return (
-
-
-
-
-
+
)
}
diff --git a/src/registry/contracts/zoodle.ts b/src/registry/contracts/zoodle.ts
new file mode 100644
index 00000000000..04be218fb88
--- /dev/null
+++ b/src/registry/contracts/zoodle.ts
@@ -0,0 +1,77 @@
+import { defineContract, defineService } from '@kittycad/registry'
+import type { ReadonlySignal } from '@preact/signals-core'
+import { TRIM_PREVIEW_LINE_COLOR } from '@src/lib/constants'
+
+export const ZOODLE_BRUSH_SIZE_MIN_PX = 1
+export const ZOODLE_BRUSH_SIZE_DEFAULT_PX = 2
+export const ZOODLE_BRUSH_SIZE_MAX_PX = 12
+
+export type ZoodleDrawToolDefinition = {
+ type: 'draw'
+ label: string
+ color: string
+ colorClassName?: string
+ lineWidthMultiplier?: number
+}
+
+export type ZoodleEraseToolDefinition = {
+ type: 'erase'
+ label: string
+ lineWidthMultiplier: number
+}
+
+export type ZoodleToolDefinition =
+ | ZoodleDrawToolDefinition
+ | ZoodleEraseToolDefinition
+
+export const zoodleToolDefinitions = {
+ drawOrange: {
+ type: 'draw',
+ label: 'Orange',
+ color: TRIM_PREVIEW_LINE_COLOR,
+ },
+ drawGreen: {
+ type: 'draw',
+ label: 'Green',
+ color: '#29FFA4',
+ },
+ drawZooBlue: {
+ type: 'draw',
+ label: 'Zoo blue',
+ color: 'currentColor',
+ colorClassName: 'text-primary',
+ },
+ drawDefault: {
+ type: 'draw',
+ label: 'Default',
+ color: 'currentColor',
+ colorClassName: 'text-default',
+ },
+ erase: {
+ type: 'erase',
+ label: 'Erase',
+ lineWidthMultiplier: 4,
+ },
+} as const satisfies Record