Spaces:
Sleeping
Sleeping
| import { | |
| GeoShapeUtil, | |
| useDefaultColorTheme, | |
| TLGeoShape | |
| } from 'tldraw' | |
| export class HatchGeoShapeUtil extends GeoShapeUtil { | |
| static override type = 'geo' as const | |
| override component(shape: TLGeoShape) { | |
| const base = super.component(shape) | |
| const theme = useDefaultColorTheme() | |
| // Get custom colors if set | |
| const fillColorKey = (shape.meta.fillColor as string) || shape.props.color | |
| const strokeThemeColor = (theme as any)[shape.props.color] | |
| const fillThemeColor = (theme as any)[fillColorKey] | |
| const strokeColor = strokeThemeColor?.solid || '#000000' | |
| const fillColor = fillThemeColor?.solid || strokeColor | |
| const borderOpacity = (shape.meta.borderOpacity as number) ?? 1.0 | |
| const fillOpacity = (shape.meta.fillOpacity as number) ?? 1.0 | |
| // Use CSS color override for all fills | |
| return ( | |
| <div | |
| style={{ | |
| ['--custom-stroke-color' as any]: strokeColor, | |
| ['--custom-fill-color' as any]: fillColor, | |
| ['--custom-stroke-opacity' as any]: borderOpacity, | |
| ['--custom-fill-opacity' as any]: fillOpacity, | |
| }} | |
| className="custom-geo-colors" | |
| > | |
| {base} | |
| </div> | |
| ) | |
| } | |
| } | |
| // Old code removed - using CSS color override instead | |
| /* function ColorOverlayRenderer({ shape, base }: { shape: TLGeoShape, base: JSX.Element }) { | |
| const theme = useDefaultColorTheme() | |
| const containerRef = useRef<HTMLDivElement>(null) | |
| const fillColorKey = (shape.meta.fillColor as string) || shape.props.color | |
| const strokeColor = theme[shape.props.color]?.solid || '#000000' | |
| const fillColor = (fillColorKey && theme[fillColorKey as keyof typeof theme]?.solid) || strokeColor | |
| console.log('🔍 Theme lookup:', { | |
| fillColorKey, | |
| themeKeys: Object.keys(theme), | |
| themeHasFillColor: fillColorKey in theme, | |
| fillColorResult: fillColor, | |
| strokeColorResult: strokeColor | |
| }) | |
| const borderOpacity = (shape.meta.borderOpacity as number) ?? 1.0 | |
| const fillOpacity = (shape.meta.fillOpacity as number) ?? 1.0 | |
| const masterOpacity = shape.opacity | |
| useLayoutEffect(() => { | |
| if (!containerRef.current) return | |
| console.log('🎨 ColorOverlayRenderer running:', { | |
| stroke: shape.props.color, | |
| fill: fillColorKey, | |
| strokeColor, | |
| fillColor, | |
| borderOpacity, | |
| fillOpacity, | |
| masterOpacity | |
| }) | |
| // Find all SVG elements in the base component | |
| const svgElements = containerRef.current.querySelectorAll('svg path, svg rect, svg ellipse, svg circle, svg polygon, svg polyline') | |
| console.log(`🎨 Found ${svgElements.length} SVG elements to recolor`) | |
| let appliedCount = 0 | |
| svgElements.forEach((el: any, index) => { | |
| const currentFill = el.getAttribute('fill') | |
| const currentStroke = el.getAttribute('stroke') | |
| console.log(`🎨 Element [${index}]:`, { | |
| tag: el.tagName, | |
| currentFill, | |
| currentStroke, | |
| willApplyFill: currentFill && currentFill !== 'none', | |
| willApplyStroke: currentStroke && currentStroke !== 'none' | |
| }) | |
| // Apply fill color and opacity | |
| if (currentFill && currentFill !== 'none') { | |
| el.setAttribute('fill', fillColor) | |
| el.style.fillOpacity = (masterOpacity * fillOpacity).toString() | |
| el.style.visibility = 'visible' | |
| appliedCount++ | |
| console.log(`🎨 ✓ Applied fill: ${fillColor} with opacity ${(masterOpacity * fillOpacity)}`) | |
| } | |
| // Apply stroke color and opacity | |
| if (currentStroke && currentStroke !== 'none') { | |
| el.setAttribute('stroke', strokeColor) | |
| el.style.strokeOpacity = (masterOpacity * borderOpacity).toString() | |
| el.style.visibility = 'visible' | |
| appliedCount++ | |
| console.log(`🎨 ✓ Applied stroke: ${strokeColor} with opacity ${(masterOpacity * borderOpacity)}`) | |
| } | |
| }) | |
| console.log(`🎨 Applied colors to ${appliedCount} attributes`) | |
| }, [shape.props.color, fillColorKey, strokeColor, fillColor, borderOpacity, fillOpacity, masterOpacity, shape.props.w, shape.props.h, shape.props.geo, shape.props.fill]) | |
| return ( | |
| <div | |
| ref={containerRef} | |
| style={{ | |
| position: 'relative', | |
| width: '100%', | |
| height: '100%', | |
| pointerEvents: 'all' | |
| }} | |
| > | |
| {base} | |
| </div> | |
| ) | |
| } | |
| */ | |
| /* function HatchRenderer_OLD({ shape }: { shape: TLGeoShape }) { | |
| const theme = useDefaultColorTheme() | |
| const { w, h, color, geo, dash, size, fill } = shape.props | |
| const masterOpacity = shape.opacity | |
| const borderOpacityMeta = (shape.meta.borderOpacity as number) ?? 1.0 | |
| const fillOpacityMeta = (shape.meta.fillOpacity as number) ?? 0.6 | |
| const svgRef = useRef<SVGSVGElement>(null) | |
| const hatchGroupRef = useRef<SVGGElement | null>(null) | |
| const strokeGroupRef = useRef<SVGGElement | null>(null) | |
| // Effect 1: Heavy RoughJS rendering (only on structural changes) | |
| useLayoutEffect(() => { | |
| const el = svgRef.current | |
| if (!el) return | |
| el.innerHTML = '' | |
| const rc = rough.svg(el) | |
| const strokeWidths = { s: 2, m: 3.5, l: 5, xl: 10 } | |
| const strokeWidth = strokeWidths[size] || 2 | |
| const hachureGaps = { s: 4, m: 5, l: 7, xl: 10 } | |
| const hachureGap = hachureGaps[size] || 5 | |
| let currentFillColor = 'none' | |
| let currentFillStyle = 'hachure' | |
| // Use separate fill color from meta.fillColor if set, otherwise use stroke color | |
| const fillColorKey = (shape.meta.fillColor as string) || color | |
| console.log('🎨 HatchRenderer:', { | |
| geo, | |
| strokeColor: color, | |
| fillColor: fillColorKey, | |
| fill, | |
| hasMeta: !!shape.meta.fillColor | |
| }) | |
| if (fill === 'solid') { | |
| currentFillColor = theme[fillColorKey as any]?.solid || theme[color].solid | |
| currentFillStyle = 'solid' | |
| } else if (fill === 'semi') { | |
| currentFillColor = theme[fillColorKey as any]?.semi || theme[color].semi | |
| currentFillStyle = 'solid' | |
| } else if (fill === 'pattern') { | |
| currentFillColor = theme[fillColorKey as any]?.semi || theme[color].semi | |
| currentFillStyle = 'hachure' | |
| } | |
| console.log('🎨 Rendering colors:', { | |
| strokeColor: theme[color].solid, | |
| fillColor: currentFillColor | |
| }) | |
| // Stroke always uses the native tldraw color style | |
| const strokeColor = theme[color].solid | |
| const hatchOptions: any = { | |
| fill: currentFillColor, | |
| stroke: 'none', | |
| fillStyle: currentFillStyle, | |
| hachureAngle: 60, | |
| hachureGap: hachureGap, | |
| roughness: 1, | |
| fillWeight: strokeWidth / 2, | |
| } | |
| const strokeOptions: any = { | |
| stroke: strokeColor, | |
| strokeWidth: strokeWidth, | |
| roughness: 1, | |
| } | |
| let dashArray: number[] | null = null | |
| if (dash === 'dashed') { | |
| dashArray = [strokeWidth * 3, strokeWidth * 3] | |
| } else if (dash === 'dotted') { | |
| dashArray = [strokeWidth, strokeWidth * 2] | |
| } | |
| const padding = strokeWidth / 2 + 1 | |
| let hatchNode: SVGGElement | null = null | |
| let strokeNode: SVGGElement | null = null | |
| // Render based on geometry type | |
| switch (geo) { | |
| case 'rectangle': | |
| case 'check-box': | |
| case 'x-box': | |
| if (fill !== 'none') hatchNode = rc.rectangle(padding, padding, w - padding * 2, h - padding * 2, hatchOptions) | |
| strokeNode = rc.rectangle(padding, padding, w - padding * 2, h - padding * 2, strokeOptions) | |
| break | |
| case 'ellipse': | |
| case 'oval': | |
| if (fill !== 'none') hatchNode = rc.ellipse(w / 2, h / 2, w - padding * 2, h - padding * 2, hatchOptions) | |
| strokeNode = rc.ellipse(w / 2, h / 2, w - padding * 2, h - padding * 2, strokeOptions) | |
| break | |
| case 'triangle': | |
| const trianglePoints: [number, number][] = [[w / 2, padding], [w - padding, h - padding], [padding, h - padding]] | |
| if (fill !== 'none') hatchNode = rc.polygon(trianglePoints, hatchOptions) | |
| strokeNode = rc.polygon(trianglePoints, strokeOptions) | |
| break | |
| case 'diamond': | |
| const diamondPoints: [number, number][] = [[w / 2, padding], [w - padding, h / 2], [w / 2, h - padding], [padding, h / 2]] | |
| if (fill !== 'none') hatchNode = rc.polygon(diamondPoints, hatchOptions) | |
| strokeNode = rc.polygon(diamondPoints, strokeOptions) | |
| break | |
| case 'pentagon': | |
| const pentagonPoints: [number, number][] = [ | |
| [w / 2, padding], | |
| [w - padding, h / 2 - h / 10], | |
| [w - padding - w / 5, h - padding], | |
| [padding + w / 5, h - padding], | |
| [padding, h / 2 - h / 10] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(pentagonPoints, hatchOptions) | |
| strokeNode = rc.polygon(pentagonPoints, strokeOptions) | |
| break | |
| case 'hexagon': | |
| const hexagonPoints: [number, number][] = [ | |
| [w / 4, padding], | |
| [w * 3 / 4, padding], | |
| [w - padding, h / 2], | |
| [w * 3 / 4, h - padding], | |
| [w / 4, h - padding], | |
| [padding, h / 2] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(hexagonPoints, hatchOptions) | |
| strokeNode = rc.polygon(hexagonPoints, strokeOptions) | |
| break | |
| case 'octagon': | |
| const octagonPoints: [number, number][] = [ | |
| [w / 3, padding], | |
| [w * 2 / 3, padding], | |
| [w - padding, w / 3], | |
| [w - padding, h - w / 3], | |
| [w * 2 / 3, h - padding], | |
| [w / 3, h - padding], | |
| [padding, h - w / 3], | |
| [padding, w / 3] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(octagonPoints, hatchOptions) | |
| strokeNode = rc.polygon(octagonPoints, strokeOptions) | |
| break | |
| case 'star': | |
| const starPoints: [number, number][] = [] | |
| const outerRadius = Math.min(w, h) / 2 - padding | |
| const innerRadius = outerRadius * 0.4 | |
| const centerX = w / 2 | |
| const centerY = h / 2 | |
| for (let i = 0; i < 10; i++) { | |
| const radius = i % 2 === 0 ? outerRadius : innerRadius | |
| const angle = (i * Math.PI / 5) - Math.PI / 2 | |
| starPoints.push([ | |
| centerX + radius * Math.cos(angle), | |
| centerY + radius * Math.sin(angle) | |
| ]) | |
| } | |
| if (fill !== 'none') hatchNode = rc.polygon(starPoints, hatchOptions) | |
| strokeNode = rc.polygon(starPoints, strokeOptions) | |
| break | |
| case 'rhombus': | |
| const rhombusPoints: [number, number][] = [ | |
| [w / 2, padding], | |
| [w - padding, h / 2], | |
| [w / 2, h - padding], | |
| [padding, h / 2] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(rhombusPoints, hatchOptions) | |
| strokeNode = rc.polygon(rhombusPoints, strokeOptions) | |
| break | |
| case 'cloud': | |
| // Simplified cloud as ellipse for now | |
| if (fill !== 'none') hatchNode = rc.ellipse(w / 2, h / 2, w - padding * 2, h - padding * 2, hatchOptions) | |
| strokeNode = rc.ellipse(w / 2, h / 2, w - padding * 2, h - padding * 2, strokeOptions) | |
| break | |
| case 'trapezoid': | |
| const trapezoidPoints: [number, number][] = [ | |
| [w / 4, padding], | |
| [w * 3 / 4, padding], | |
| [w - padding, h - padding], | |
| [padding, h - padding] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(trapezoidPoints, hatchOptions) | |
| strokeNode = rc.polygon(trapezoidPoints, strokeOptions) | |
| break | |
| case 'arrow-right': | |
| const arrowRightPoints: [number, number][] = [ | |
| [padding, padding], | |
| [w * 3 / 4, padding], | |
| [w - padding, h / 2], | |
| [w * 3 / 4, h - padding], | |
| [padding, h - padding] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(arrowRightPoints, hatchOptions) | |
| strokeNode = rc.polygon(arrowRightPoints, strokeOptions) | |
| break | |
| case 'arrow-left': | |
| const arrowLeftPoints: [number, number][] = [ | |
| [w - padding, padding], | |
| [w / 4, padding], | |
| [padding, h / 2], | |
| [w / 4, h - padding], | |
| [w - padding, h - padding] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(arrowLeftPoints, hatchOptions) | |
| strokeNode = rc.polygon(arrowLeftPoints, strokeOptions) | |
| break | |
| case 'arrow-up': | |
| const arrowUpPoints: [number, number][] = [ | |
| [w / 2, padding], | |
| [w - padding, h / 4], | |
| [w * 3 / 4, h / 4], | |
| [w * 3 / 4, h - padding], | |
| [w / 4, h - padding], | |
| [w / 4, h / 4], | |
| [padding, h / 4] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(arrowUpPoints, hatchOptions) | |
| strokeNode = rc.polygon(arrowUpPoints, strokeOptions) | |
| break | |
| case 'arrow-down': | |
| const arrowDownPoints: [number, number][] = [ | |
| [w / 4, padding], | |
| [w * 3 / 4, padding], | |
| [w * 3 / 4, h * 3 / 4], | |
| [w - padding, h * 3 / 4], | |
| [w / 2, h - padding], | |
| [padding, h * 3 / 4], | |
| [w / 4, h * 3 / 4] | |
| ] | |
| if (fill !== 'none') hatchNode = rc.polygon(arrowDownPoints, hatchOptions) | |
| strokeNode = rc.polygon(arrowDownPoints, strokeOptions) | |
| break | |
| default: | |
| // Default to rectangle for unknown shapes | |
| if (fill !== 'none') hatchNode = rc.rectangle(padding, padding, w - padding * 2, h - padding * 2, hatchOptions) | |
| strokeNode = rc.rectangle(padding, padding, w - padding * 2, h - padding * 2, strokeOptions) | |
| break | |
| } | |
| if (hatchNode) { | |
| el.appendChild(hatchNode) | |
| hatchGroupRef.current = hatchNode | |
| } else { | |
| hatchGroupRef.current = null | |
| } | |
| if (strokeNode) { | |
| if (dashArray) { | |
| const dashString = dashArray.join(', ') | |
| strokeNode.querySelectorAll('path').forEach(path => { | |
| path.setAttribute('stroke-dasharray', dashString) | |
| }) | |
| } | |
| el.appendChild(strokeNode) | |
| strokeGroupRef.current = strokeNode | |
| } else { | |
| strokeGroupRef.current = null | |
| } | |
| }, [w, h, color, geo, theme, dash, size, fill, shape.meta.fillColor]) | |
| // Effect 2: Fast opacity updates (no RoughJS path generation) | |
| useLayoutEffect(() => { | |
| if (hatchGroupRef.current) { | |
| hatchGroupRef.current.setAttribute('opacity', (masterOpacity * fillOpacityMeta).toString()) | |
| } | |
| if (strokeGroupRef.current) { | |
| strokeGroupRef.current.setAttribute('opacity', (masterOpacity * borderOpacityMeta).toString()) | |
| } | |
| }, [masterOpacity, fillOpacityMeta, borderOpacityMeta]) | |
| return ( | |
| <svg | |
| ref={svgRef} | |
| style={{ | |
| position: 'absolute', | |
| top: 0, | |
| left: 0, | |
| width: w, | |
| height: h, | |
| overflow: 'visible', | |
| pointerEvents: 'none' | |
| }} | |
| /> | |
| ) | |
| } | |
| */ | |