Skip to content

Commit

Permalink
feat: bold geometry line 657d392
Browse files Browse the repository at this point in the history
  • Loading branch information
plantain-00 committed Mar 18, 2024
1 parent e8485da commit 338a60c
Show file tree
Hide file tree
Showing 15 changed files with 259 additions and 72 deletions.
39 changes: 24 additions & 15 deletions dev/attributed-opentype-text-editor.story.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import React from "react"
import { Align, AttributedText, BooleanEditor, Button, EnumEditor, MapCache3, NumberEditor, ObjectEditor, PathCommand, StringEditor, VerticalAlign, aligns, reactSvgRenderTarget, useAttributedTextEditor, useWindowSize, verticalAligns } from "../src"
import { Align, AttributedText, BooleanEditor, Button, EnumEditor, MapCache4, NumberEditor, ObjectEditor, PathCommand, StringEditor, VerticalAlign, aligns, boldGeometryLines, geometryLineToPathCommands, pathCommandsToGeometryLines, reactSvgRenderTarget, useAttributedTextEditor, useWindowSize, verticalAligns } from "../src"
import * as opentype from 'opentype.js'
import { allFonts, opentypeCommandsToPathCommands } from "./opentype/utils"
import { CopyData } from "./cad-editor/plugins/copy-paste.plugin"
// import SanJiKaiShu from './vender/SanJiKaiShu-2.ttf'
// allFonts[0].url = SanJiKaiShu

export default () => {
type Attribute = Partial<{ color: number, fontSize: number, backgroundColor: number, underline: boolean, passThrough: boolean, script?: 'sub' | 'sup', circle?: boolean, stackText?: string, bold?: boolean, italic?: boolean }>
type Attribute = Partial<{ color: number, fontSize: number, backgroundColor: number, underline: boolean, passThrough: boolean, script?: 'sub' | 'sup', circle?: boolean, stackText?: string, bold?: boolean, italic?: boolean, opacity?: number }>
const size = useWindowSize()
const width = size.width / 2 - 30
const [font, setFont] = React.useState<opentype.Font>()
const cache = React.useRef(new MapCache3<string, number, boolean, { commands: PathCommand[], x1: number, y1: number, width: number }>())
const getTextLayout = (text: string, fontSize: number, italic = false) => {
const cache = React.useRef(new MapCache4<boolean, boolean, number, string, { commands: PathCommand[], x1: number, y1: number, width: number }>())
const getTextLayout = (text: string, fontSize: number, italic = false, bold = false) => {
if (!font || !text) return
return cache.current.get(text, fontSize, italic, () => {
return cache.current.get(italic, bold, fontSize, text, () => {
const path = font.getPath(text, 0, fontSize, fontSize, { xScale: fontSize / font.unitsPerEm, yScale: fontSize / font.unitsPerEm })
const glyph = font.charToGlyph(text)
const box = glyph.getBoundingBox()
const advanceWidth = glyph.advanceWidth || 0
const width = box.x2 - box.x1
const commands = opentypeCommandsToPathCommands(path, italic ? fontSize * 0.7 : undefined)
let commands = opentypeCommandsToPathCommands(path, italic ? fontSize * 0.7 : undefined)
if (bold && commands.length > 0) {
commands = boldGeometryLines(pathCommandsToGeometryLines(commands)).map(lines => geometryLineToPathCommands(lines)).flat()
}
return {
commands,
x1: (advanceWidth > width ? 0 : box.x1) / font.unitsPerEm * fontSize,
Expand All @@ -29,10 +32,12 @@ export default () => {
}
})
}
const [state, setState] = React.useState<AttributedText<Attribute>[]>([{ insert: '我们出' }, { insert: '去吧', attributes: { stackText: 'ab' } }, { insert: 'Aag jioI', attributes: { color: 0xff0000 } }])
const [state, setState] = React.useState<AttributedText<Attribute>[]>([{ insert: '我们出' }, { insert: '去吧', attributes: { stackText: 'ab' } }, { insert: 'Aag jioIb BD', attributes: { color: 0xff0000 } }])
const [align, setAlign] = React.useState<Align>('left')
const [verticalAlign, setVerticalAlign] = React.useState<VerticalAlign>('top')
const [strokeOnly, setStrokeOnly] = React.useState(false)
const getColor = (content: AttributedText<Attribute>) => content?.attributes?.color ?? 0x000000
const getOpacity = (content: AttributedText<Attribute>) => content?.attributes?.opacity ?? 1
const getFontSize = (content?: AttributedText<Attribute>) => content?.attributes?.fontSize ?? 50
const getComputedFontSize = (content?: AttributedText<Attribute>) => getFontSize(content) * (content?.attributes?.script || content?.attributes?.stackText ? 0.7 : 1)
const getBackgroundColor = (content?: AttributedText<Attribute>) => content?.attributes?.backgroundColor ?? 0xffffff
Expand Down Expand Up @@ -89,18 +94,20 @@ export default () => {
}
const fontSize = getComputedFontSize(content)
const italic = getItalic(content)
const layout = getTextLayout(content.insert, fontSize, italic)
const bold = getBold(content)
const layout = getTextLayout(content.insert, fontSize, italic, bold)
if (layout) {
const color = getColor(content)
const opacity = getOpacity(content)
const backgroundColor = getBackgroundColor(content)
if (!selected && backgroundColor !== 0xffffff) {
children.push(target.renderRect(x, y, width, lineHeight, { fillColor: backgroundColor, strokeWidth: 0 }))
}
if (getUnderline(content)) {
children.push(target.renderPolyline([{ x, y: y + lineHeight }, { x: x + width, y: y + lineHeight }], { strokeColor: color }))
children.push(target.renderPolyline([{ x, y: y + lineHeight }, { x: x + width, y: y + lineHeight }], { strokeColor: color, strokeOpacity: opacity }))
}
if (getPassThrough(content)) {
children.push(target.renderPolyline([{ x, y: y + lineHeight / 2 }, { x: x + width, y: y + lineHeight / 2 }], { strokeColor: color }))
children.push(target.renderPolyline([{ x, y: y + lineHeight / 2 }, { x: x + width, y: y + lineHeight / 2 }], { strokeColor: color, strokeOpacity: opacity }))
}
const pos = {
x: x - layout.x1,
Expand All @@ -119,25 +126,25 @@ export default () => {
}
if (getCircle(content)) {
pos.x += (lineHeight - getWidth(content)) / 2
children.push(target.renderCircle(x + width / 2, y + lineHeight / 2, lineHeight / 2, { strokeColor: color }))
children.push(target.renderCircle(x + width / 2, y + lineHeight / 2, lineHeight / 2, { strokeColor: color, strokeOpacity: opacity }))
}
const strokeWidth = getBold(content) ? 2 : 0
children.push(target.renderGroup([target.renderPathCommands(layout.commands, { fillColor: color, strokeColor: color, strokeWidth })], { translate: pos }))
const style = strokeOnly ? { strokeColor: color, strokeOpacity: opacity, strokeWidth: 1 } : { fillColor: color, fillOpacity: opacity, strokeWidth: 0 }
children.push(target.renderGroup([target.renderPathCommands(layout.commands, style)], { translate: pos }))
if (selected) {
commands.push(layout.commands)
}
if (stackText) {
const stackWidth = stackText.split('').reduce((p, c) => p + (getTextLayout(c, fontSize)?.width ?? 0), 0)
let xOffset = 0
for (const char of stackText.split('')) {
const stackLayout = getTextLayout(char, fontSize, italic)
const stackLayout = getTextLayout(char, fontSize, italic, bold)
if (stackLayout) {
const stackPos = {
x: x - stackLayout.x1 + (width - stackWidth) / 2 + xOffset,
y: y + stackLayout.y1 + (lineHeight - getLineHeight(content)) + fontSize * 0.2,
}
xOffset += stackLayout.width
children.push(target.renderGroup([target.renderPathCommands(stackLayout.commands, { fillColor: color, strokeColor: color, strokeWidth })], { translate: stackPos }))
children.push(target.renderGroup([target.renderPathCommands(stackLayout.commands, style)], { translate: stackPos }))
}
}
}
Expand All @@ -153,12 +160,14 @@ export default () => {
inline
properties={{
color: <NumberEditor type="color" value={getColor(cursorContent)} setValue={v => setSelectedAttributes({ color: v })} />,
opacity: <NumberEditor value={getOpacity(cursorContent)} setValue={v => setSelectedAttributes({ opacity: v })} />,
fontSize: <NumberEditor value={getFontSize(cursorContent)} setValue={v => setSelectedAttributes({ fontSize: v })} />,
backgroundColor: <NumberEditor type="color" value={getBackgroundColor(cursorContent)} setValue={v => setSelectedAttributes({ backgroundColor: v === 0xffffff ? undefined : v })} />,
underline: <BooleanEditor value={getUnderline(cursorContent)} setValue={v => setSelectedAttributes({ underline: v ? true : undefined })} />,
passThrough: <BooleanEditor value={getPassThrough(cursorContent)} setValue={v => setSelectedAttributes({ passThrough: v ? true : undefined })} />,
bold: <BooleanEditor value={getBold(cursorContent)} setValue={v => setSelectedAttributes({ bold: v ? true : undefined })} />,
italic: <BooleanEditor value={getItalic(cursorContent)} setValue={v => setSelectedAttributes({ italic: v ? true : undefined })} />,
strokeOnly: <BooleanEditor value={strokeOnly} setValue={v => setStrokeOnly(v)} />,
sub: <BooleanEditor value={getScript(cursorContent) === 'sub'} setValue={v => setSelectedAttributes({ script: v ? 'sub' : undefined })} />,
sup: <BooleanEditor value={getScript(cursorContent) === 'sup'} setValue={v => setSelectedAttributes({ script: v ? 'sup' : undefined })} />,
circle: <BooleanEditor value={getCircle(cursorContent)} setValue={v => setSelectedAttributes({ circle: v ? true : undefined })} />,
Expand Down
2 changes: 1 addition & 1 deletion dev/cad-editor/plugins/nurbs.plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function getModel(ctx: PluginContext): model.Model<NurbsContent>[] {
},
offset(content, point, distance, contents) {
const lines = getNurbsGeometries(content, contents).lines
return ctx.getParallelGeometryLinesByDistance(point, lines, distance).map(r => ctx.geometryLineToContent(r))
return ctx.getParallelGeometryLinesByDistancePoint(point, lines, distance).map(r => ctx.geometryLineToContent(r))
},
render(content, renderCtx) {
const { points } = getNurbsGeometries(content, renderCtx.contents)
Expand Down
2 changes: 1 addition & 1 deletion dev/cad-editor/plugins/path.plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function getModel(ctx: PluginContext): model.Model<PathContent> {
const lines = getPathGeometriesFromCache(content, contents).lines
return {
...content,
commands: ctx.geometryLineToPathCommands(ctx.getParallelGeometryLinesByDistance(point, lines, distance)),
commands: ctx.geometryLineToPathCommands(ctx.getParallelGeometryLinesByDistancePoint(point, lines, distance)),
}
},
render(content, renderCtx) {
Expand Down
2 changes: 1 addition & 1 deletion dev/cad-editor/plugins/pline.plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function getModel(ctx: PluginContext) {
},
offset(content, point, distance, contents) {
const { lines } = getPlineGeometries(content, contents)
const newLines = ctx.getParallelGeometryLinesByDistance(point, lines, distance)
const newLines = ctx.getParallelGeometryLinesByDistancePoint(point, lines, distance)
return ctx.geometryLinesToPline(newLines)
},
join(content, target, contents) {
Expand Down
6 changes: 3 additions & 3 deletions dev/cad-editor/plugins/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6690,7 +6690,7 @@ function getModel(ctx) {
},
offset(content, point, distance, contents) {
const lines = getNurbsGeometries(content, contents).lines;
return ctx.getParallelGeometryLinesByDistance(point, lines, distance).map((r) => ctx.geometryLineToContent(r));
return ctx.getParallelGeometryLinesByDistancePoint(point, lines, distance).map((r) => ctx.geometryLineToContent(r));
},
render(content, renderCtx) {
const { points } = getNurbsGeometries(content, renderCtx.contents);
Expand Down Expand Up @@ -7383,7 +7383,7 @@ function getModel(ctx) {
const lines = getPathGeometriesFromCache(content, contents).lines;
return {
...content,
commands: ctx.geometryLineToPathCommands(ctx.getParallelGeometryLinesByDistance(point, lines, distance))
commands: ctx.geometryLineToPathCommands(ctx.getParallelGeometryLinesByDistancePoint(point, lines, distance))
};
},
render(content, renderCtx) {
Expand Down Expand Up @@ -8019,7 +8019,7 @@ function getModel(ctx) {
},
offset(content, point, distance, contents) {
const { lines } = getPlineGeometries(content, contents);
const newLines = ctx.getParallelGeometryLinesByDistance(point, lines, distance);
const newLines = ctx.getParallelGeometryLinesByDistancePoint(point, lines, distance);
return ctx.geometryLinesToPline(newLines);
},
join(content, target, contents) {
Expand Down
2 changes: 1 addition & 1 deletion main.bundle.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-size.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
composable-type-validator: 5.29e+3
react-render-target: 1.60e+5
react-render-target: 1.62e+5
use-undo-redo: 1.38e+3
use-patch-based-undo-redo: 4.10e+3
react-composable-json-editor: 2.00e+4
Expand All @@ -12,4 +12,4 @@ use-drag-resize: 6.71e+3
use-drag-rotate: 2.75e+3
equation-renderer: 1.21e+4
equation-solver: 7.33e+4
equation-calculater: 6.06e+3
equation-calculater: 6.88e+3
8 changes: 4 additions & 4 deletions spec/intersection.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ Generated by [AVA](https://avajs.dev).
[
{
x: 50.000000000000014,
y: 9.184850993605152e-15,
x: 0,
y: 100.00000000000001,
},
{
x: -7.105427357601002e-15,
y: 100.00000000000001,
x: 50.00000000000001,
y: 1.83697019872103e-14,
},
]

Expand Down
Binary file modified spec/intersection.ts.snap
Binary file not shown.
32 changes: 30 additions & 2 deletions src/utils/equation-calculater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ export function calculateEquation2(a: number, b: number, c: number, delta?: numb
if (isZero(a, delta)) {
return calculateEquation1(b, c, delta)
}
if (isZero(c, delta)) {
const remains = calculateEquation1(a, b, delta)
if (remains.some(r => isZero(r, delta))) {
return remains
}
return [0, ...remains]
}
if (a !== 1) {
b /= a
c /= a
Expand All @@ -42,6 +49,13 @@ export function calculateEquation3(a: number, b: number, c: number, d: number, d
if (isZero(a, delta)) {
return calculateEquation2(b, c, d, delta)
}
if (isZero(d, delta)) {
const remains = calculateEquation2(a, b, c, delta)
if (remains.some(r => isZero(r, delta))) {
return remains
}
return [0, ...remains]
}
if (a !== 1) {
b /= a
c /= a
Expand Down Expand Up @@ -87,6 +101,13 @@ export function calculateEquation4(a: number, b: number, c: number, d: number, e
if (isZero(a, delta)) {
return calculateEquation3(b, c, d, e, delta)
}
if (isZero(e, delta)) {
const remains = calculateEquation3(a, b, c, d, delta)
if (remains.some(r => isZero(r, delta))) {
return remains
}
return [0, ...remains]
}
if (a !== 1) {
b /= a
c /= a
Expand Down Expand Up @@ -192,6 +213,13 @@ export function calculateEquation5(params: number[], x0: number, delta = delta2,
if (params.length <= 5) {
return calculateEquation4(params[params.length - 5] || 0, params[params.length - 4] || 0, params[params.length - 3] || 0, params[params.length - 2] || 0, params[params.length - 1] || 0, delta)
}
if (isZero(params[params.length - 1], delta)) {
const remains = calculateEquation5(params.slice(0, params.length - 1), delta)
if (remains.some(r => isZero(r, delta))) {
return remains
}
return [0, ...remains]
}
const f1 = (x: number) => {
let result = 0
for (const p of params) {
Expand Down Expand Up @@ -225,8 +253,8 @@ export function calculateEquation5(params: number[], x0: number, delta = delta2,

export function newtonIterate(
x0: number,
f1:(x: number) => number,
f2:(x: number) => number,
f1: (x: number) => number,
f2: (x: number) => number,
delta: number,
maxIteratorCount = 100,
) {
Expand Down
87 changes: 81 additions & 6 deletions src/utils/geometry-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import { getNurbsCurveDerivatives, getNurbsCurveParamAtPoint, getNurbsCurvePoint
import { Position, deduplicatePosition, getPointByLengthAndDirection, getTwoPointsDistance, isSamePoint } from "./position";
import { Path, ValidationResult, validate, tuple } from "./validators";
import { getAngleInRange, AngleRange } from "./angle";
import { deduplicate, equals, first, isBetween, isSameNumber, isZero, largerThan, lessOrEqual, lessThan } from "./math";
import { deduplicate, equals, first, isBetween, isSameNumber, isZero, largerThan, lessOrEqual, lessThan, maxmiumBy } from "./math";
import { radianToAngle, getTwoPointsRadian, angleToRadian } from "./radian";
import { getArcTangentRadianAtRadian, getEllipseArcTangentRadianAtRadian, getQuadraticCurveTangentRadianAtPercent, getBezierCurveTangentRadianAtPercent } from "./tangency";
import { reverseGeometryLines } from "./reverse";
import { reverseClosedGeometryLinesIfAreaIsNegative, reverseGeometryLines } from "./reverse";
import { getGeometryLinesPoints } from "./hatch";
import { getParallelGeometryLinesByDistanceDirectionIndex, pointSideToIndex } from "./parallel";

export type GeometryLine = [Position, Position] |
{ type: 'arc'; curve: Arc } |
Expand Down Expand Up @@ -363,7 +364,9 @@ export function splitGeometryLines(lines: GeometryLine[]): GeometryLine[][] {
...getPartOfGeometryLines(params[1], lines.length, lines),
...getPartOfGeometryLines(0, params[0], lines),
]
if (part1.length === 0) continue
const part2 = getPartOfGeometryLines(params[0], params[1], lines)
if (part2.length === 0) continue
const area1 = getPolygonArea(getGeometryLinesPoints(part1))
if (!isZero(area1)) {
if (area1 < 0) continue
Expand All @@ -379,11 +382,83 @@ export function splitGeometryLines(lines: GeometryLine[]): GeometryLine[][] {
return [lines]
}

export function optimizeGeometryLine(line: GeometryLine): GeometryLine | undefined {
if (Array.isArray(line)) {
if (isSamePoint(...line)) return
} else if (line.type === 'arc') {
if (isSameNumber(line.curve.startAngle, line.curve.endAngle)) return
if (lessOrEqual(line.curve.r, 0)) return
} else if (line.type === 'ellipse arc') {
if (isSameNumber(line.curve.startAngle, line.curve.endAngle)) return
if (lessOrEqual(line.curve.rx, 0)) return
if (lessOrEqual(line.curve.ry, 0)) return
if (isSameNumber(line.curve.rx, line.curve.ry)) {
return optimizeGeometryLine({
type: 'arc',
curve: {
r: line.curve.rx,
x: line.curve.cx,
y: line.curve.cy,
startAngle: line.curve.startAngle,
endAngle: line.curve.endAngle,
counterclockwise: line.curve.counterclockwise,
}
})
}
} else if (line.type === 'quadratic curve') {
if (pointIsOnLine(line.curve.cp, line.curve.from, line.curve.to)) {
return optimizeGeometryLine([line.curve.from, line.curve.to])
}
} else if (line.type === 'bezier curve') {
if (pointIsOnLine(line.curve.cp1, line.curve.from, line.curve.to) && pointIsOnLine(line.curve.cp2, line.curve.from, line.curve.to)) {
return optimizeGeometryLine([line.curve.from, line.curve.to])
}
} else if (line.type === 'nurbs curve') {
if (line.curve.points.length < 2) return
if (line.curve.points.length === 2) {
return optimizeGeometryLine([line.curve.points[0], line.curve.points[1]])
}
}
return line
}

export function optimizeGeometryLines(lines: GeometryLine[]): GeometryLine[] {
return lines.filter(line => {
if (Array.isArray(line)) {
return !isSamePoint(...line)
const result: GeometryLine[] = []
for (const line of lines) {
const r = optimizeGeometryLine(line)
if (r) {
result.push(r)
}
}
return result
}

export function boldGeometryLines(lines: GeometryLine[][], distance = 1): GeometryLine[][] {
const polygons = lines.map(n => {
const points = deduplicatePosition(getGeometryLinesPoints(n, 10))
return {
points,
lines: n,
direction: Math.sign(getPolygonArea(points)),
}
})
const baseDirection = (polygons.find(p => polygons.every(e => e === p || !pointInPolygon(p.points[0], e.points))) || polygons[0]).direction
const result: GeometryLine[][] = []
for (const polygon of polygons) {
const direction = polygon.direction
result.push(getParallelGeometryLinesByDistanceDirectionIndex(polygon.lines, direction === baseDirection ? distance : -distance, pointSideToIndex(direction), 'bevel'))
}
return result.map(lines => {
const newLines = reverseClosedGeometryLinesIfAreaIsNegative(lines)
const reversed = newLines !== lines
lines = newLines
lines = maxmiumBy(splitGeometryLines(lines).map(n => ({
lines: n,
area: Math.abs(getPolygonArea(getGeometryLinesPoints(n, 10))),
})), n => n.area).lines
if (reversed) {
lines = reverseGeometryLines(lines)
}
return true
return lines
})
}
Loading

0 comments on commit 338a60c

Please sign in to comment.