Skip to content

Commit

Permalink
fixup: render SVG by flipping coordinates rather than a transform
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous committed Oct 23, 2022
1 parent b89ece6 commit 9441d48
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 81 deletions.
4 changes: 4 additions & 0 deletions packages/plotter/src/bounding-box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export function toViewBox(box: Box): ViewBox {
: [box[0], box[1], box[2] - box[0], box[3] - box[1]]
}

export function flipY(box: Box): ViewBox {
return isEmpty(box) ? [0, 0, 0, 0] : [box[0], -box[3], box[2], -box[1]]
}

export function fromGraphic(graphic: Tree.ImageGraphic): Box {
return graphic.type === Tree.IMAGE_SHAPE
? fromShape(graphic.shape)
Expand Down
20 changes: 10 additions & 10 deletions packages/renderer/src/__tests__/shape-to-element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('mapping a shape to an element', () => {

expect(result).to.deep.include({
tagName: 'circle',
properties: {cx: 1, cy: 2, r: 3},
properties: {cx: 1, cy: -2, r: 3},
children: [],
})
})
Expand All @@ -27,7 +27,7 @@ describe('mapping a shape to an element', () => {

expect(result).to.deep.include({
tagName: 'rect',
properties: {x: 1, y: 2, width: 3, height: 4},
properties: {x: 1, y: -6, width: 3, height: 4},
children: [],
})
})
Expand All @@ -45,7 +45,7 @@ describe('mapping a shape to an element', () => {

expect(result).to.deep.include({
tagName: 'rect',
properties: {x: 1, y: 2, width: 3, height: 4, rx: 0.25, ry: 0.25},
properties: {x: 1, y: -6, width: 3, height: 4, rx: 0.25, ry: 0.25},
children: [],
})
})
Expand All @@ -64,7 +64,7 @@ describe('mapping a shape to an element', () => {

expect(result).to.deep.include({
tagName: 'polygon',
properties: {points: '1,1 2,1 2,2 1,2'},
properties: {points: '1,-1 2,-1 2,-2 1,-2'},
children: [],
})
})
Expand All @@ -82,7 +82,7 @@ describe('mapping a shape to an element', () => {

expect(result).to.deep.include({
tagName: 'path',
properties: {d: 'M0 0L1 1L2 2'},
properties: {d: 'M0 0L1 -1L2 -2'},
children: [],
})
})
Expand All @@ -99,7 +99,7 @@ describe('mapping a shape to an element', () => {

expect(result).to.deep.include({
tagName: 'path',
properties: {d: 'M1 2L3 4M5 6L7 8'},
properties: {d: 'M1 -2L3 -4M5 -6L7 -8'},
children: [],
})
})
Expand Down Expand Up @@ -146,10 +146,10 @@ describe('mapping a shape to an element', () => {
properties: {
d: [
'M0 0',
'A0.25 0.25 0 0 0 0.25 0.25',
'A0.25 0.25 0 0 1 0.5 0.5',
'A0.25 0.25 0 1 0 0.75 0.25',
'A0.25 0.25 0 1 1 1 0',
'A0.25 0.25 0 0 1 0.25 -0.25',
'A0.25 0.25 0 0 0 0.5 -0.5',
'A0.25 0.25 0 1 1 0.75 -0.25',
'A0.25 0.25 0 1 0 1 0',
].join(''),
},
children: [],
Expand Down
84 changes: 24 additions & 60 deletions packages/renderer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import {s} from 'hastscript'
import {map} from 'unist-util-map'
import {visitParents} from 'unist-util-visit-parents'

import type {ImageTree, ImageNode, ImageLayer} from '@tracespace/plotter'
import {
IMAGE,
IMAGE_LAYER,
IMAGE_SHAPE,
IMAGE_PATH,
IMAGE_REGION,
BoundingBox as BBox,
} from '@tracespace/plotter'
import type {ImageTree} from '@tracespace/plotter'
import {BoundingBox as BBox} from '@tracespace/plotter'

import {renderShape, renderPath} from './render'
import {renderGraphic} from './render'
import type {SvgElement} from './types'

export type {SvgElement} from './types'
Expand All @@ -21,6 +13,9 @@ export const BASE_SVG_PROPS = {
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
}

export const BASE_IMAGE_PROPS = {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '0',
Expand All @@ -30,56 +25,25 @@ export const BASE_SVG_PROPS = {
}

export function render(image: ImageTree): SvgElement {
const svgTree = map(image, mapImageTreeToSvg)

if (svgTree.properties) {
const {width, height} = svgTree.properties

if (typeof width === 'number') {
svgTree.properties.width = `${width}${image.units}`
}

if (typeof height === 'number') {
svgTree.properties.height = `${height}${image.units}`
}
}

return svgTree
const {units} = image
const mainLayer = image.children[0]
const {size} = mainLayer
const viewBox = BBox.toViewBox(BBox.flipY(size))

return s(
'svg',
{
...BASE_SVG_PROPS,
viewBox: viewBox.join(' '),
width: `${viewBox[2]}${units}`,
height: `${viewBox[3]}${units}`,
},
[renderFragment(image)]
)
}

function mapImageTreeToSvg(node: ImageNode): SvgElement {
switch (node.type) {
case IMAGE: {
let box = BBox.empty()
visitParents(node, IMAGE_LAYER, (layer: ImageLayer) => {
box = BBox.add(box, layer.size)
})
const [xMin, yMin, width, height] = BBox.toViewBox(box)
const props = {
...BASE_SVG_PROPS,
width,
height,
viewBox: `${xMin} ${yMin} ${width} ${height}`,
}
return s('svg', props)
}

case IMAGE_LAYER: {
const vbox = BBox.toViewBox(node.size)
return s('g', {
transform: `translate(0, ${vbox[3] + 2 * vbox[1]}) scale(1,-1)`,
})
}

case IMAGE_SHAPE: {
return renderShape(node)
}

case IMAGE_PATH:
case IMAGE_REGION: {
return renderPath(node)
}
}
export function renderFragment(image: ImageTree): SvgElement {
const mainLayer = image.children[0]

return s('metadata', [JSON.stringify(node)])
return s('g', {...BASE_IMAGE_PROPS}, mainLayer.children.map(renderGraphic))
}
37 changes: 26 additions & 11 deletions packages/renderer/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {s} from 'hastscript'

import {random as createId} from '@tracespace/xml-id'
import type {
ImageGraphic,
ImageShape,
ImagePath,
ImageRegion,
Expand All @@ -11,7 +12,7 @@ import type {
import {
BoundingBox,
positionsEqual,
OutlineShape,
IMAGE_SHAPE,
IMAGE_PATH,
CIRCLE,
RECTANGLE,
Expand All @@ -24,6 +25,14 @@ import {

import type {SvgElement} from './types'

export function renderGraphic(node: ImageGraphic): SvgElement {
if (node.type === IMAGE_SHAPE) {
return renderShape(node)
}

return renderPath(node)
}

export function renderShape(node: ImageShape): SvgElement {
const {shape} = node

Expand All @@ -34,16 +43,24 @@ export function shapeToElement(shape: Shape): SvgElement {
switch (shape.type) {
case CIRCLE: {
const {cx, cy, r} = shape
return s('circle', {cx, cy, r})
return s('circle', {cx, cy: -cy, r})
}

case RECTANGLE: {
const {x, y, xSize: width, ySize: height, r} = shape
return s('rect', {x, y, width, height, rx: r, ry: r})
return s('rect', {
x,
y: -y - height,
width,
height,
rx: r,
ry: r,
})
}

case POLYGON: {
const points = shape.points.map(p => p.join(',')).join(' ')
const points = shape.points.map(([x, y]) => `${x},${-y}`).join(' ')

return s('polygon', {points})
}

Expand All @@ -59,9 +76,7 @@ export function shapeToElement(shape: Shape): SvgElement {

for (const [i, layerShape] of shape.shapes.entries()) {
if (layerShape.erase && !BoundingBox.isEmpty(boundingBox)) {
const [bx1, by1, bx2, by2] = boundingBox
const clipId = `${clipIdBase}__${i}`
const boundingPath = `M${bx1} ${by1} H${bx2} V${by2} H${bx1} V${by1}`

defs.push(s('clipPath', {id: clipId}, [shapeToElement(layerShape)]))
children = [s('g', {clipPath: `url(#${clipId})`}, children)]
Expand Down Expand Up @@ -97,29 +112,29 @@ function segmentsToPathData(segments: PathSegment[]): string {
const {start, end} = next

if (!previous || !positionsEqual(previous.end, start)) {
pathCommands.push(`M${start[0]} ${start[1]}`)
pathCommands.push(`M${start[0]} ${-start[1]}`)
}

if (next.type === LINE) {
pathCommands.push(`L${end[0]} ${end[1]}`)
pathCommands.push(`L${end[0]} ${-end[1]}`)
} else if (next.type === ARC) {
const sweep = next.end[2] - next.start[2]
const absSweep = Math.abs(sweep)
const {center, radius} = next

// Sweep flag flipped from SVG value because Y-axis is positive-down
const sweepFlag = sweep < 0 ? '0' : '1'
const sweepFlag = sweep < 0 ? '1' : '0'
let largeFlag = absSweep <= Math.PI ? '0' : '1'

// A full circle needs two SVG arcs to draw
if (absSweep === 2 * Math.PI) {
const [mx, my] = [2 * center[0] - end[0], 2 * center[1] - end[1]]
const [mx, my] = [2 * center[0] - end[0], -(2 * center[1] - end[1])]
largeFlag = '0'
pathCommands.push(`A${radius} ${radius} 0 0 ${sweepFlag} ${mx} ${my}`)
}

pathCommands.push(
`A${radius} ${radius} 0 ${largeFlag} ${sweepFlag} ${end[0]} ${end[1]}`
`A${radius} ${radius} 0 ${largeFlag} ${sweepFlag} ${end[0]} ${-end[1]}`
)
}
}
Expand Down

0 comments on commit 9441d48

Please sign in to comment.