WS-2 R3: containers/preamble as background backdrops, tick-N, no false overlap flag
Some checks failed
app-ci-deploy / test-build-deploy (push) Has been cancelled
Some checks failed
app-ci-deploy / test-build-deploy (push) Has been cancelled
From KC's 2nd review: - z-order: boundaries, preamble bands and container frames drawn behind; figures, responses and parts on top (a figure's description tooltip is no longer blocked by the preamble box). Preamble + containers are pointerEvents:none. - the 'large box around all content' is gone: a container renders as a small corner tab (label + total marks), not an enclosing box; intermediate part-groups keep a faint dashed nesting frame. Main-question extent is shown by its boundaries. - MC 'tick N' count (select_n) surfaced next to the option boxes. - fixed the spurious 'overlapping shapes' review flag on containers (they legitimately enclose their children; only genuine partial overlaps between non-containers flag now). tsc clean; 7/7 model tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GruxHXxfdp4kZCgAMVFgvV
This commit is contained in:
parent
81e1b5ed25
commit
c346493a34
@ -154,11 +154,23 @@ function bringDomainShapesToFront(editor: Editor) {
|
||||
if (ids.length) try { editor.bringToFront(ids as any) } catch { /* */ }
|
||||
}
|
||||
|
||||
// Draw order = z-order (tldraw stacks by creation). Backdrops (boundaries, preamble bands, container frames)
|
||||
// go first/behind so the figures, responses and parts on top stay hoverable and legible.
|
||||
function zPriority(m: ExamCanvasShapeModel): number {
|
||||
if (m.kind === 'boundary') return 0
|
||||
if (m.kind === 'context' && m.contextType === 'preamble') return 1
|
||||
if (m.kind === 'part' && m.isContainer) return 2
|
||||
if (m.kind === 'part') return 3
|
||||
if (m.kind === 'context') return 5 // figures/tables on top so their description tooltip is reachable
|
||||
return 4 // responses + other regions
|
||||
}
|
||||
|
||||
function loadShapes(editor: Editor, models: ExamCanvasShapeModel[]) {
|
||||
const existing = editor.getCurrentPageShapes().filter((s) => shapeTypeToKind(s.type)).map((s) => s.id)
|
||||
if (existing.length) editor.deleteShapes(existing)
|
||||
if (!models.length) return
|
||||
editor.createShapes(models.map((m) => ({
|
||||
const ordered = [...models].sort((a, b) => zPriority(a) - zPriority(b))
|
||||
editor.createShapes(ordered.map((m) => ({
|
||||
id: createShapeId(m.id),
|
||||
type: SHAPE_TYPES[m.kind],
|
||||
x: m.x,
|
||||
|
||||
@ -184,6 +184,11 @@ function renderResponseSubstructure(shape: ExamCanvasTLShape): React.ReactNode {
|
||||
const filled = Number(b.fill ?? 0) > 0.35
|
||||
els.push(<div key={`bx${i}`} title={`option ${i + 1}${filled ? ' (marked)' : ''}`} style={{ position: 'absolute', left: pct(rel.x0), top: pct(rel.y0), width: pct(Number(rel.x1) - Number(rel.x0)), height: pct(Number(rel.y1) - Number(rel.y0)), border: '1.5px solid var(--exam-stroke)', borderRadius: 3, background: filled ? 'var(--exam-stroke)' : 'transparent', opacity: filled ? 0.5 : 0.85, boxSizing: 'border-box' }} />)
|
||||
})
|
||||
// "tick N" instruction for multiple-choice (select_n = how many boxes to tick)
|
||||
const selectN = Number(m.select_n ?? 0)
|
||||
if (selectN > 0 && boxes.length) {
|
||||
els.push(<span key="tickn" className="exam-canvas-shape__pill" style={{ position: 'absolute', left: 4, top: 2, fontSize: 10, fontWeight: 900, borderRadius: 6, padding: '0 5px', opacity: 0.95 }}>tick {selectN}</span>)
|
||||
}
|
||||
const fal = m.final_answer_line
|
||||
if (fal && (fal.unit || fal.quantity)) {
|
||||
els.push(<span key="fal" className="exam-canvas-shape__pill" style={{ position: 'absolute', right: 4, top: `calc(${pct(fal.rel_y, 0.9)} - 8px)`, maxWidth: 'calc(100% - 8px)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontSize: 10, fontWeight: 800, borderRadius: 6, padding: '0 5px', opacity: 0.95 }} title="expected final answer">{[fal.quantity, fal.unit && `(${fal.unit})`].filter(Boolean).join(' ')}</span>)
|
||||
@ -191,6 +196,29 @@ function renderResponseSubstructure(shape: ExamCanvasTLShape): React.ReactNode {
|
||||
return els.length ? <>{els}</> : null
|
||||
}
|
||||
|
||||
// A container question is a BACKDROP, not a big box over the content: the main question (depth 0) relies on
|
||||
// its start/end boundary rules for extent and shows only a small corner tab (label + total marks); an
|
||||
// intermediate part-group (depth ≥ 1) also gets a faint dashed frame to convey nesting. Non-interactive so it
|
||||
// never blocks a hover on the figures/responses on top.
|
||||
function renderContainerTab(shape: ExamCanvasTLShape) {
|
||||
const depth = shape.props.depth ?? 0
|
||||
const stroke = containerStroke(depth)
|
||||
const isAiSuggestion = shape.props.source === 'ai' && shape.props.confirmed === false
|
||||
const marks = shape.props.maxMarks
|
||||
const title = provenanceTitle(shape, depth === 0 ? 'Question (container)' : 'Part group (container)')
|
||||
return (
|
||||
<HTMLContainer id={shape.id} style={{ width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h), pointerEvents: 'none', overflow: 'visible' }}>
|
||||
<style>{shapeCss}</style>
|
||||
{depth > 0 && <div style={{ position: 'absolute', inset: 0, border: `1px dashed ${stroke}`, borderRadius: 12, opacity: 0.3, boxSizing: 'border-box' }} />}
|
||||
<span className="exam-canvas-shape__pill" title={title} style={{ position: 'absolute', left: 0, top: -3, display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 11, fontWeight: 900, borderRadius: 8, padding: '2px 7px', color: stroke, border: `1.5px solid ${stroke}`, background: 'rgba(255,255,255,0.92)', whiteSpace: 'nowrap' }}>
|
||||
<span aria-hidden="true">{isAiSuggestion ? 'AI' : (depth === 0 ? '▣' : '▢')}</span>
|
||||
{shape.props.label}
|
||||
{typeof marks === 'number' && marks > 0 && <span style={{ opacity: 0.85 }}>· {marks}m</span>}
|
||||
</span>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function renderShape(shape: ExamCanvasTLShape) {
|
||||
const kind = shape.props.kind
|
||||
const p = canvasShapePalette[kind] ?? canvasShapePalette.response
|
||||
@ -198,6 +226,8 @@ function renderShape(shape: ExamCanvasTLShape) {
|
||||
const isAiSuggestion = shape.props.source === 'ai' && shape.props.confirmed === false
|
||||
if (isBoundary) return renderBoundaryLine(shape)
|
||||
const isContainer = shape.props.isContainer === true && kind === 'part'
|
||||
if (isContainer) return renderContainerTab(shape)
|
||||
const isPreamble = kind === 'context' && shape.props.contextType === 'preamble'
|
||||
const depth = shape.props.depth ?? 0
|
||||
const stroke = isContainer ? containerStroke(depth) : p.stroke
|
||||
const darkStroke = isContainer ? containerStroke(depth) : p.darkStroke
|
||||
@ -206,7 +236,7 @@ function renderShape(shape: ExamCanvasTLShape) {
|
||||
const roleLabel = isContainer ? (depth === 0 ? 'Question: container' : 'Part: container') : `${p.label}: ${p.role}`
|
||||
const title = shape.props.description ? `${provenanceTitle(shape, roleLabel)} • ${shape.props.description}` : provenanceTitle(shape, roleLabel)
|
||||
return (
|
||||
<HTMLContainer id={shape.id} style={{ width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h), pointerEvents: 'all' }}>
|
||||
<HTMLContainer id={shape.id} style={{ width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h), pointerEvents: isPreamble ? 'none' : 'all' }}>
|
||||
<style>{shapeCss}</style>
|
||||
<div
|
||||
className={`exam-canvas-shape exam-canvas-shape--${kind}${isContainer ? ' exam-canvas-shape--container' : ''}`}
|
||||
|
||||
@ -288,7 +288,9 @@ function addCheapReviewFlags(shapes: ExamCanvasShapeModel[], pages?: CanvasPageG
|
||||
if (typeof shape.confidence === 'number' && shape.confidence < 0.7) flags.push('low confidence')
|
||||
if ((shape.kind === 'part' || shape.kind === 'question_number') && looksUncertainLabel(shape.label)) flags.push('uncertain question label')
|
||||
if (shape.kind === 'part' && (!shape.maxMarks || shape.maxMarks <= 0) && !markAreasByQuestion.has(shape.questionId ?? shape.id)) flags.push('missing marks')
|
||||
const samePageOverlap = shapes.some((other, otherIndex) => otherIndex !== index && shape.kind !== 'boundary' && other.kind !== 'boundary' && pageForShape(shape, pages) === pageForShape(other, pages) && overlaps(shape, other) && (shape.kind === other.kind || (!contains(bounds(shape), bounds(other)) && !contains(bounds(other), bounds(shape)))))
|
||||
// Containers are MEANT to enclose their children, so never flag them (or their children) for overlap;
|
||||
// flag only genuine partial overlaps between non-container shapes where neither contains the other.
|
||||
const samePageOverlap = !shape.isContainer && shape.kind !== 'boundary' && shapes.some((other, otherIndex) => otherIndex !== index && !other.isContainer && other.kind !== 'boundary' && pageForShape(shape, pages) === pageForShape(other, pages) && overlaps(shape, other) && !contains(bounds(shape), bounds(other)) && !contains(bounds(other), bounds(shape)))
|
||||
if (samePageOverlap) flags.push('overlapping shapes')
|
||||
return flags.length ? { ...shape, reviewFlags: flags } : shape
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user