Some checks failed
app-ci-deploy / test-build-deploy (push) Has been cancelled
- updateUserPreferences: isDarkMode → colorScheme ('dark'|'light') per
tldraw 3.6.1 TLUserPreferences type (isDarkMode is read-only computed)
- BaseBoxShapeUtil subclasses: add required indicator() method returning
bounding rect; fixes non-abstract class missing abstract member error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
6.7 KiB
TypeScript
92 lines
6.7 KiB
TypeScript
|
|
import React from 'react'
|
|
import { BaseBoxShapeTool, BaseBoxShapeUtil, HTMLContainer, T, TLBaseBoxShape, toDomPrecision } from '@tldraw/tldraw'
|
|
import type { ExamCanvasRegionKind, ExamCanvasShapeKind } from '../../../utils/exam-canvas/model'
|
|
|
|
export const SHAPE_TYPES = {
|
|
boundary: 'exam-boundary',
|
|
part: 'exam-part',
|
|
response: 'exam-region-response',
|
|
context: 'exam-region-context',
|
|
question_number: 'exam-region-question-number',
|
|
mark_area: 'exam-region-mark-area',
|
|
reference: 'exam-region-reference',
|
|
furniture: 'exam-region-furniture',
|
|
} as const
|
|
|
|
export type ExamCanvasTLShape = TLBaseBoxShape & {
|
|
type: typeof SHAPE_TYPES[keyof typeof SHAPE_TYPES]
|
|
props: {
|
|
w: number
|
|
h: number
|
|
label: string
|
|
kind: ExamCanvasShapeKind
|
|
maxMarks?: number
|
|
responseForm?: string
|
|
contextType?: string
|
|
questionId?: string | null
|
|
domainId?: string
|
|
}
|
|
}
|
|
|
|
const palette: Record<ExamCanvasShapeKind, { stroke: string; fill: string; dash?: string; label: string }> = {
|
|
boundary: { stroke: '#ef4444', fill: 'rgba(239,68,68,0.06)', dash: '8 6', label: 'Boundary' },
|
|
part: { stroke: '#f59e0b', fill: 'rgba(245,158,11,0.16)', label: 'Part' },
|
|
response: { stroke: '#2563eb', fill: 'rgba(37,99,235,0.16)', label: 'Response' },
|
|
context: { stroke: '#7c3aed', fill: 'rgba(124,58,237,0.14)', dash: '6 5', label: 'Context' },
|
|
question_number: { stroke: '#0f766e', fill: 'rgba(15,118,110,0.14)', label: 'Question #' },
|
|
mark_area: { stroke: '#16a34a', fill: 'rgba(22,163,74,0.14)', label: 'Marks' },
|
|
reference: { stroke: '#0891b2', fill: 'rgba(8,145,178,0.14)', label: 'Reference' },
|
|
furniture: { stroke: '#64748b', fill: 'rgba(100,116,139,0.12)', dash: '3 5', label: 'Furniture' },
|
|
}
|
|
|
|
function renderShape(shape: ExamCanvasTLShape) {
|
|
const kind = shape.props.kind
|
|
const p = palette[kind] ?? palette.response
|
|
const isBoundary = kind === 'boundary'
|
|
return (
|
|
<HTMLContainer id={shape.id} style={{ width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h), pointerEvents: 'all' }}>
|
|
<div style={{
|
|
width: '100%', height: '100%', boxSizing: 'border-box', border: `${isBoundary ? 2 : 1.5}px solid ${p.stroke}`,
|
|
borderStyle: p.dash ? 'dashed' : 'solid', borderRadius: isBoundary ? 999 : 10,
|
|
background: isBoundary ? 'transparent' : p.fill, color: p.stroke, fontFamily: 'Inter, system-ui, sans-serif',
|
|
display: 'flex', alignItems: isBoundary ? 'center' : 'flex-start', justifyContent: isBoundary ? 'center' : 'space-between',
|
|
padding: isBoundary ? '0 8px' : 8, boxShadow: isBoundary ? 'none' : '0 10px 22px rgba(15,23,42,0.08)', overflow: 'hidden'
|
|
}}>
|
|
<span style={{ fontSize: 12, fontWeight: 800, textTransform: 'uppercase', letterSpacing: 0.6, background: 'rgba(255,255,255,0.84)', borderRadius: 999, padding: '2px 7px' }}>
|
|
{shape.props.label || p.label}
|
|
</span>
|
|
{!isBoundary && shape.props.questionId && <span style={{ fontSize: 11, fontWeight: 700, opacity: .75 }}>Attached</span>}
|
|
</div>
|
|
</HTMLContainer>
|
|
)
|
|
}
|
|
|
|
function defaultProps(kind: ExamCanvasShapeKind, w: number, h: number) {
|
|
const p = palette[kind]
|
|
return { w, h, label: p.label, kind, responseForm: kind === 'response' ? 'lines' : undefined, contextType: kind === 'context' ? 'generic' : undefined }
|
|
}
|
|
|
|
const sharedProps = { w: T.number, h: T.number, label: T.string, kind: T.string, maxMarks: T.optional(T.number), responseForm: T.optional(T.string), contextType: T.optional(T.string), questionId: T.optional(T.string), domainId: T.optional(T.string) }
|
|
const ind = (s: ExamCanvasTLShape) => <rect width={toDomPrecision(s.props.w)} height={toDomPrecision(s.props.h)} />
|
|
class BoundaryUtil extends BaseBoxShapeUtil<ExamCanvasTLShape> { static override type = SHAPE_TYPES.boundary; static override props = sharedProps; override getDefaultProps(){ return defaultProps('boundary', 680, 8) }; override component(shape: ExamCanvasTLShape){ return renderShape(shape) }; override indicator(shape: ExamCanvasTLShape){ return ind(shape) } }
|
|
class PartUtil extends BaseBoxShapeUtil<ExamCanvasTLShape> { static override type = SHAPE_TYPES.part; static override props = sharedProps; override getDefaultProps(){ return defaultProps('part', 420, 170) }; override component(shape: ExamCanvasTLShape){ return renderShape(shape) }; override indicator(shape: ExamCanvasTLShape){ return ind(shape) } }
|
|
function regionUtil(type: string, kind: ExamCanvasRegionKind, w = 360, h = 120) { return class extends BaseBoxShapeUtil<ExamCanvasTLShape> { static override type = type; static override props = sharedProps; override getDefaultProps(){ return defaultProps(kind, w, h) }; override component(shape: ExamCanvasTLShape){ return renderShape(shape) }; override indicator(shape: ExamCanvasTLShape){ return ind(shape) } } }
|
|
|
|
class BoundaryTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.boundary; static override initial = 'pointing'; shapeType = SHAPE_TYPES.boundary }
|
|
class PartTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.part; static override initial = 'pointing'; shapeType = SHAPE_TYPES.part }
|
|
class ResponseTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.response; static override initial = 'pointing'; shapeType = SHAPE_TYPES.response }
|
|
class ContextTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.context; static override initial = 'pointing'; shapeType = SHAPE_TYPES.context }
|
|
class QuestionNumberTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.question_number; static override initial = 'pointing'; shapeType = SHAPE_TYPES.question_number }
|
|
class MarkAreaTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.mark_area; static override initial = 'pointing'; shapeType = SHAPE_TYPES.mark_area }
|
|
class ReferenceTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.reference; static override initial = 'pointing'; shapeType = SHAPE_TYPES.reference }
|
|
class FurnitureTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.furniture; static override initial = 'pointing'; shapeType = SHAPE_TYPES.furniture }
|
|
|
|
export const examCanvasShapeUtils = [BoundaryUtil, PartUtil, regionUtil(SHAPE_TYPES.response, 'response'), regionUtil(SHAPE_TYPES.context, 'context'), regionUtil(SHAPE_TYPES.question_number, 'question_number', 170, 80), regionUtil(SHAPE_TYPES.mark_area, 'mark_area', 170, 80), regionUtil(SHAPE_TYPES.reference, 'reference'), regionUtil(SHAPE_TYPES.furniture, 'furniture')] as const
|
|
export const examCanvasTools = [BoundaryTool, PartTool, ResponseTool, ContextTool, QuestionNumberTool, MarkAreaTool, ReferenceTool, FurnitureTool] as const
|
|
|
|
export function shapeTypeToKind(type: string): ExamCanvasShapeKind | null {
|
|
const entry = Object.entries(SHAPE_TYPES).find(([, v]) => v === type)
|
|
return (entry?.[0] as ExamCanvasShapeKind | undefined) ?? null
|
|
}
|