Skip to content

Commit

Permalink
Treat U+1BC30..U+1BC31 more like curves
Browse files Browse the repository at this point in the history
U+1BC30 DUPLOYAN LETTER J N and U+1BC31 DUPLOYAN LETTER J N S represent
ligatures of pairs of curve letters. They are too complex to implement
with `Curve`, but anything that applies to curves probably also applies
to them, so they are now implemented with a new class, `ComplexCurve`.
It is a subclass of `Complex` that provides a `Curve`-like interface.
  • Loading branch information
dscorbett committed Feb 4, 2025
1 parent ca06131 commit 2790948
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 41 deletions.
7 changes: 4 additions & 3 deletions sources/charsets/data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2018-2019, 2022-2024 David Corbett
# Copyright 2018-2019, 2022-2025 David Corbett
# Copyright 2019-2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -34,6 +34,7 @@
from shapes import Bound
from shapes import Circle
from shapes import Complex
from shapes import ComplexCurve
from shapes import Curve
from shapes import Dot
from shapes import EqualsSign
Expand Down Expand Up @@ -329,8 +330,8 @@ def initialize_schemas(charset: Charset, light_line: float, stroke_gap: float) -
s_n = Curve(0, 90, clockwise=False, secondary=True, may_reposition_cursive_endpoints=True)
k_r_s = Curve(90, 180, clockwise=False)
s_k = Curve(90, 0, clockwise=True, secondary=False, may_reposition_cursive_endpoints=True)
j_n = Complex([(1, s_k), (1, n)])
j_n_s = Complex([(3, s_k), (4, n_s)])
j_n = ComplexCurve([(1, s_k), (1, n)])
j_n_s = ComplexCurve([(3, s_k), (4, n_s)])
o = Circle(90, 90, clockwise=False)
o_reverse = o.as_reversed()
ie = Curve(180, 0, clockwise=False, may_reposition_cursive_endpoints=True)
Expand Down
86 changes: 49 additions & 37 deletions sources/phases/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from shapes import Circle
from shapes import CircleRole
from shapes import Complex
from shapes import ComplexCurve
from shapes import ContextMarker
from shapes import ContinuingOverlap
from shapes import ContinuingOverlapS
Expand Down Expand Up @@ -1908,59 +1909,70 @@ def avoid_cochiral_overlaps(
)
probably_smoothable_schemas: OrderedSet[Schema] = OrderedSet()
maximum_unsmoothable_size = float('-inf')
for schema in new_schemas:
for schema in schemas:
if (schema.glyph_class == GlyphClass.JOINER
and isinstance(schema.path, Curve) and schema.path.angle_in % 90 == 0 and schema.path.angle_out % 90 == 0
and isinstance(schema.path, ComplexCurve | Curve)
and schema.path.angle_in % 90 == 0 and schema.path.angle_out % 90 == 0
and abs(schema.path.get_da()) % 360 > 90
and not schema.path.reversed_circle
and schema.path.entry_position == 1 and schema.path.exit_position == 1
):
if schema.joining_type == Type.JOINING:
if schema.joining_type == Type.JOINING and schema in new_schemas:
if not (schema.path.smooth_1 or schema.path.smooth_2):
probably_smoothable_schemas.add(schema)
elif schema.path.smooth_2:
classes['s2'].append(schema)
else:
maximum_unsmoothable_size = max(maximum_unsmoothable_size, schema.size)
elif schema.path.smooth_2 and schema not in original_schemas:
classes['c2'].append(schema)
elif schema in original_schemas:
maximum_unsmoothable_size = max(
maximum_unsmoothable_size,
schema.size * (schema.path.instructions[0].size if isinstance(schema.path, ComplexCurve) else 1), # type: ignore[union-attr]
)
contexts_1: OrderedSet[str] = OrderedSet()
contexts_2: OrderedSet[str] = OrderedSet()
inputs: OrderedSet[tuple[Schema, str, str]] = OrderedSet()
inputs: OrderedSet[tuple[Schema, str, str, str]] = OrderedSet()
for schema in probably_smoothable_schemas:
if schema.size <= maximum_unsmoothable_size:
if maximum_unsmoothable_size >= schema.size * (schema.path.instructions[0].size if isinstance(schema.path, ComplexCurve) else 1): # type: ignore[union-attr]
continue
assert isinstance(schema.path, Curve)
context_1 = f'{schema.path.angle_out}_{schema.path.clockwise}'
assert isinstance(schema.path, ComplexCurve | Curve)
context_1 = f'c1_{schema.path.angle_out}_{schema.path.clockwise}'
contexts_1.add(context_1)
classes[f'c_{context_1}'].append(schema)
context_2 = f'{(schema.path.angle_in + 90 * (1 if schema.path.clockwise else -1)) % 360}_{schema.path.clockwise}'
classes[context_1].append(schema)
context_2 = f'c2_{schema.path.angle_in}_{schema.path.clockwise}'
contexts_2.add(context_2)
inputs.add((schema, context_1, context_2))
for classifying in [True, False]:
for schema, context_2, context_1 in inputs:
assert isinstance(schema.path, Curve)
context_2 = f'{schema.path.angle_out}_{schema.path.clockwise}'
context_1 = f'{(schema.path.angle_in + 90 * (1 if schema.path.clockwise else -1)) % 360}_{schema.path.clockwise}'
input_class = f'i_{context_1}'
if classifying:
classes[input_class].append(schema)
if context_1 in contexts_1 and context_2 in contexts_2:
output_class = f'o12_{context_1}'
if classifying:
classes[output_class].append(schema.clone(cmap=None, path=schema.path.clone(smooth_1=True, smooth_2=True)))
classes[context_2].append(schema)
offset_context_1 = f'c2_{(schema.path.angle_out - 90 * (1 if schema.path.clockwise else -1)) % 360}_{schema.path.clockwise}'
offset_context_2 = f'c1_{(schema.path.angle_in + 90 * (1 if schema.path.clockwise else -1)) % 360}_{schema.path.clockwise}'
inputs.add((schema, context_2, offset_context_1, offset_context_2))
for iteration in range(3):
for schema, context_2, offset_context_1, offset_context_2 in inputs:
assert isinstance(schema.path, ComplexCurve | Curve)
if iteration != 2 and offset_context_1 in contexts_2 and offset_context_2 in contexts_1:
input_class = f'i12_{context_2}'
if iteration == 0:
classes[input_class].append(schema)
output_class = f'o12_{context_2}'
if iteration == 0:
classes[output_class].append(schema.clone(cmap=None, path=schema.path.smooth(smooth_1=True, smooth_2=True)))
else:
add_rule(lookup, Rule(f'c_{context_1}', input_class, 's2', output_class))
if context_2 in contexts_2:
output_class = f'o1_{context_1}'
if classifying:
classes[output_class].append(schema.clone(cmap=None, path=schema.path.clone(smooth_1=True)))
add_rule(lookup, Rule(offset_context_2, input_class, 'c2', output_class))
if iteration != 1 and offset_context_1 in contexts_2:
input_class = 'i1'
if iteration == 0:
classes[input_class].append(schema)
output_class = 'o1'
if iteration == 0:
classes[output_class].append(schema.clone(cmap=None, path=schema.path.smooth(smooth_1=True)))
else:
add_rule(lookup, Rule([], input_class, 's2', output_class))
if context_1 in contexts_1:
output_class = f'o2_{context_1}'
if classifying:
classes[output_class].append(schema.clone(cmap=None, path=schema.path.clone(smooth_2=True)))
add_rule(lookup, Rule([], input_class, 'c2', output_class))
if iteration != 1 and offset_context_2 in contexts_1:
input_class = f'i2_{context_2}'
if iteration == 0:
classes[input_class].append(schema)
output_class = f'o2_{context_2}'
if iteration == 0:
classes[output_class].append(schema.clone(cmap=None, path=schema.path.smooth(smooth_2=True)))
else:
add_rule(lookup, Rule(f'c_{context_1}', input_class, [], output_class))
add_rule(lookup, Rule(offset_context_2, input_class, [], output_class))
return [lookup]


Expand Down
109 changes: 108 additions & 1 deletion sources/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,22 @@ def clone(
smooth_2=self.smooth_2 if smooth_2 is CLONE_DEFAULT else smooth_2,
)

def smooth(
self,
*,
smooth_1: CloneDefault | bool = CLONE_DEFAULT,
smooth_2: CloneDefault | bool = CLONE_DEFAULT,
) -> Self:
"""Returns a copy of this shape with the ends smoothed.
Args:
smooth_1: The `smooth_1` value to use when cloning this
shape.
smooth_2: The `smooth_2` value to use when cloning this
shape.
"""
return self.clone(smooth_1=smooth_1, smooth_2=smooth_2)

@override
def get_name(self, size: float, joining_type: Type) -> str:
if self.overlap_angle is not None:
Expand Down Expand Up @@ -2202,7 +2218,7 @@ def get_da(
Returns:
The difference between this curve’s entry angle and exit
angle in the range (0, 360].
angle. If the difference is 0, the return value is 360.
"""
return self._get_normalized_angles_and_da(False, False, angle_in, angle_out)[2]

Expand Down Expand Up @@ -3779,6 +3795,97 @@ def calculate_diacritic_angles(self) -> Mapping[str, float]:
return super().calculate_diacritic_angles()


class ComplexCurve(Complex):
"""A sequence of multiple cochiral curves.
"""

@override
def __init__(self, instructions: Instructions) -> None:
super().__init__(instructions)
assert len(instructions) >= 2, 'Not enough instructions: {len(instructions)}'
assert all(
not callable(op) and isinstance(op.shape, Curve) and not op.shape.reversed_circle
and op.shape.clockwise is self.instructions[0].shape.clockwise # type: ignore[union-attr]
for op in self.instructions
), f'Invalid instructions for `ComplexCurve`: {instructions}'
self._first_curve: Curve = self.instructions[0].shape # type: ignore[assignment, union-attr]
self._last_curve: Curve = self.instructions[-1].shape # type: ignore[assignment, union-attr]

@override
def get_name(self, size: float, joining_type: Type) -> str:
name = super().get_name(size, joining_type)
if self.smooth_1 or self.smooth_2:
name += f'''{
'.' if name else ''
}s{
'1' if self.smooth_1 else ''
}{
'2' if self.smooth_2 else ''
}'''
return name

@property
def angle_in(self) -> float:
return self._first_curve.angle_in

@property
def angle_out(self) -> float:
return self._last_curve.angle_out

@property
def clockwise(self) -> bool:
return self._last_curve.clockwise

@property
def reversed_circle(self) -> float:
return self._first_curve.reversed_circle

@property
def entry_position(self) -> float:
return self._first_curve.entry_position

@property
def exit_position(self) -> float:
return self._last_curve.exit_position

@property
def smooth_1(self) -> bool:
return self._last_curve.smooth_1

@property
def smooth_2(self) -> bool:
return self._first_curve.smooth_2

def get_da(self) -> float:
"""Returns the difference between the entry and exit angles.
Returns:
The difference between this curve’s entry angle and exit
angle. If the difference is 0, the return value is 360.
"""
return self._last_curve.get_da(self.angle_in)

def smooth(
self,
*,
smooth_1: CloneDefault | bool = CLONE_DEFAULT,
smooth_2: CloneDefault | bool = CLONE_DEFAULT,
) -> Self:
"""Returns a copy of this shape with the ends smoothed.
Args:
smooth_1: The `smooth_1` value to use when cloning this
shape’s last curve.
smooth_2: The `smooth_2` value to use when cloning this
shape’s first curve.
"""
return self.clone(instructions=[
self.instructions[0]._replace(shape=self._first_curve.clone(smooth_2=smooth_2)), # type: ignore[union-attr]
*self.instructions[1:-1],
self.instructions[-1]._replace(shape=self._last_curve.clone(smooth_1=smooth_1)), # type: ignore[union-attr]
])


class RotatedComplex(Complex):
"""A shape made by rotating another shape.
"""
Expand Down
4 changes: 4 additions & 0 deletions tests/cochiral-sequences.test
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
# limitations under the License.

1BC1A 1BC1B::[u1BC1A.n.s1@0,0|u1BC1B.j.s2@0,0|_@1068,0]
1BC1A 1BC30::[u1BC1A.n.s1@0,0|u1BC30.j_n.s2@0,-420|_@964,0]
1BC30 1BC30 1BC30::[u1BC30.j_n.s1@0,0|u1BC30.j_n.s12@300,-529|u1BC30.j_n.s2@724,-950|_@1688,0]
1BC1C 1BC19::[u1BC1C.s.s1@0,0|u1BC19.m.s2@529,-529|_@1069,0]
1BC28 1BC29::[u1BC28.n_s.s1@0,0|u1BC29.j_s.s2@0,0|_@1784,0]
1BC28 1BC31::[u1BC28.n_s.s1@0,0|u1BC31.j_n_s.s2@0,-1140|_@1064,0]
1BC31 1BC31 1BC31::[u1BC31.j_n_s.s1@0,0|u1BC31.j_n_s.s12@300,-1244|u1BC31.j_n_s.s2@724,-2385|_@1788,0]
1BC2A 1BC27::[u1BC2A.s_s.s1@0,0|u1BC27.m_s.s2@1144,-1144|_@1784,0]
1BC79 1BC2A 1BC21::[u1BC79.tail.s1@0,0|u1BC2A.s_s.s12@0,429|u1BC19.m.s2@1347,0|dupl_.Dot.1.rel1@1633,346|_@1901,0]

0 comments on commit 2790948

Please sign in to comment.