mirror of
https://github.com/google/pebble.git
synced 2025-03-21 03:11:21 +00:00
337 lines
13 KiB
Python
337 lines
13 KiB
Python
# Copyright 2024 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
'''
|
|
JSON2COMMANDS creates Pebble Draw Commands (the Python Objects, _not_ a serialized .pdc) from a JSON file.
|
|
Currently only the PathCommand is supported.
|
|
|
|
The JSON file can contain multiple frames (i.e. PDC Sequence).
|
|
Each frame is composed of 'fillGroups'.
|
|
A fillGroup may be: An individual filled polygon (a.k.a. a fill), or _all_ unfilled polylines (a.k.a. all open paths).
|
|
Each fillGroup is parsed separately and a list of Pebble Draw Commands that describe it is created.
|
|
The created list should have the length of the lowest number of commands possible in order to draw that fillGroup.
|
|
|
|
Currently, there is no support for a JSON to contain the viewbox size or fill colors.
|
|
The viewbox size is currently passed in as a parameter.
|
|
The fill color is currently defaulted to solid white.
|
|
'''
|
|
|
|
import os
|
|
import argparse
|
|
import pebble_commands
|
|
import json
|
|
import graph
|
|
from itertools import groupby
|
|
|
|
INVISIBLE_POINT_THRESHOLD = 500
|
|
DISPLAY_DIM_X = 144
|
|
DISPLAY_DIM_Y = 168
|
|
OPEN_PATH_TAG = "_"
|
|
|
|
|
|
def parse_color(color_opacity, truncate):
|
|
if color_opacity is None:
|
|
return 0
|
|
|
|
r = int(round(255 * color_opacity[0]))
|
|
g = int(round(255 * color_opacity[1]))
|
|
b = int(round(255 * color_opacity[2]))
|
|
a = int(round(255 * color_opacity[3]))
|
|
|
|
return pebble_commands.convert_color(r, g, b, a, truncate)
|
|
|
|
|
|
def parse_json_line_data(json_line_data, viewbox_size=(DISPLAY_DIM_X, DISPLAY_DIM_Y)):
|
|
# A list of one-way vectors, but intended to store their negatives at all times.
|
|
bidirectional_lines = []
|
|
|
|
for line_data in json_line_data:
|
|
# Skip invisible lines
|
|
if abs(line_data['startPoint'][0]) > INVISIBLE_POINT_THRESHOLD or \
|
|
abs(line_data['startPoint'][1]) > INVISIBLE_POINT_THRESHOLD or \
|
|
abs(line_data['endPoint'][0]) > INVISIBLE_POINT_THRESHOLD or \
|
|
abs(line_data['endPoint'][1]) > INVISIBLE_POINT_THRESHOLD:
|
|
continue
|
|
|
|
# Center the viewbox of all lines (by moving the lines' absolute
|
|
# coordinates relative to the screen)
|
|
dx = -(DISPLAY_DIM_X - viewbox_size[0]) / 2
|
|
dy = -(DISPLAY_DIM_Y - viewbox_size[1]) / 2
|
|
start_point = (line_data["startPoint"][0] + dx, line_data["startPoint"][1] + dy)
|
|
end_point = (line_data["endPoint"][0] + dx, line_data["endPoint"][1] + dy)
|
|
|
|
# Since lines are represented and stored as one-way vectors, but may be
|
|
# drawn in either direction, all operations must be done on their reverse
|
|
line = (start_point, end_point)
|
|
reverse_line = (end_point, start_point)
|
|
|
|
# Skip duplicate lines
|
|
if line in bidirectional_lines:
|
|
continue
|
|
|
|
bidirectional_lines.append(line)
|
|
bidirectional_lines.append(reverse_line)
|
|
|
|
return bidirectional_lines
|
|
|
|
|
|
def determine_longest_path(bidirectional_lines):
|
|
'''
|
|
Returns the longest path in 'bidirectional_lines', and removes all its segments from 'bidirectional_lines'
|
|
If 'bidirectional_lines' contains more than one possible longest path, only one will be returned.
|
|
'''
|
|
# Construct graph out of bidirectional_lines
|
|
g = graph.Graph({})
|
|
for line in bidirectional_lines:
|
|
g.add_edge(line)
|
|
|
|
# Find longest path
|
|
longest_path_length = 0
|
|
longest_path = []
|
|
vertices = g.get_vertices()
|
|
for i in range(len(vertices)):
|
|
start_vertex = vertices[i]
|
|
|
|
for j in range(i, len(vertices)):
|
|
end_vertex = vertices[j]
|
|
paths = g.find_all_paths(start_vertex, end_vertex)
|
|
for path in paths:
|
|
if (len(path) - 1) > longest_path_length:
|
|
longest_path = path
|
|
longest_path_length = len(path) - 1
|
|
|
|
# Edge case - Line is a point
|
|
if len(longest_path) == 1:
|
|
longest_path = [longest_path, longest_path]
|
|
|
|
# Remove longest_path's line segments from bidirectional_lines
|
|
# Since bidirectional_lines is a list of one-way vectors but represents
|
|
# bidirectional lines, a line segment and its reverse must be removed to
|
|
# keep its integrity
|
|
for k in range(len(longest_path) - 1):
|
|
path_line = (longest_path[k], longest_path[k + 1])
|
|
reverse_path_line = (path_line[1], path_line[0])
|
|
bidirectional_lines.remove(path_line)
|
|
bidirectional_lines.remove(reverse_path_line)
|
|
|
|
return longest_path
|
|
|
|
|
|
def process_unique_group_of_lines(unique_group_data, translate, viewbox_size, path_open, stroke_width, stroke_color, fill_color, precise, raise_error):
|
|
'''
|
|
Creates a list of commands that draw out a unique group of lines.
|
|
|
|
A unique group of lines is defined as having a unique stroke width, stroke color, and fill.
|
|
Note that this does _not_ guarantee the group may be described by a single Pebble Draw Command.
|
|
'''
|
|
|
|
unique_group_commands = []
|
|
|
|
bidirectional_lines = parse_json_line_data(unique_group_data, viewbox_size)
|
|
if not bidirectional_lines:
|
|
return unique_group_commands
|
|
|
|
while bidirectional_lines:
|
|
longest_path = determine_longest_path(bidirectional_lines)
|
|
|
|
try:
|
|
c = pebble_commands.PathCommand(longest_path,
|
|
path_open,
|
|
translate,
|
|
stroke_width,
|
|
stroke_color,
|
|
fill_color,
|
|
precise,
|
|
raise_error)
|
|
|
|
if c is not None:
|
|
unique_group_commands.append(c)
|
|
except pebble_commands.InvalidPointException:
|
|
raise
|
|
|
|
return unique_group_commands
|
|
|
|
|
|
def process_fill(fillGroup_data, translate, viewbox_size, path_open, precise, raise_error, truncate_color):
|
|
fill_command = []
|
|
error = False
|
|
|
|
# A fill is implicitly a unique group of lines - all line segments must have the same stroke width, stroke color
|
|
# Get line style from first line segment
|
|
stroke_width = fillGroup_data[0]['thickness']
|
|
stroke_color = parse_color(fillGroup_data[0]['color'], truncate_color)
|
|
# Fill color should be solid white until it can be inserted in the JSON
|
|
fill_color = parse_color([1, 1, 1, 1], truncate_color)
|
|
if stroke_color == 0:
|
|
stroke_width = 0
|
|
elif stroke_width == 0:
|
|
stroke_color = 0
|
|
|
|
try:
|
|
unique_group_commands = process_unique_group_of_lines(
|
|
fillGroup_data,
|
|
translate,
|
|
viewbox_size,
|
|
path_open,
|
|
stroke_width,
|
|
stroke_color,
|
|
fill_color,
|
|
precise,
|
|
raise_error)
|
|
|
|
if unique_group_commands:
|
|
fill_command += unique_group_commands
|
|
except pebble_commands.InvalidPointException:
|
|
error = True
|
|
|
|
return fill_command, error
|
|
|
|
|
|
def process_open_paths(fillGroup_data, translate, viewbox_size, path_open, precise, raise_error, truncate_color):
|
|
open_paths_commands = []
|
|
error = False
|
|
|
|
fill_color = parse_color([0, 0, 0, 0], truncate_color) # No fill color
|
|
|
|
# These open paths are part of the same fillGroup, but may have varied stroke width
|
|
fillGroup_data = sorted(fillGroup_data, key=lambda a: a['thickness'])
|
|
for stroke_width, unique_width_group in groupby(fillGroup_data, lambda c: c['thickness']):
|
|
unique_width_data = list(unique_width_group)
|
|
|
|
# These open paths have the same width, but may have varied color
|
|
unique_width_data = sorted(unique_width_data, key=lambda d: d['color'])
|
|
for stroke_color_raw, unique_width_and_color_group in groupby(unique_width_data, lambda e: e['color']):
|
|
# These are a unique group of lines
|
|
unique_width_and_color_data = list(unique_width_and_color_group)
|
|
|
|
stroke_color = parse_color(stroke_color_raw, truncate_color)
|
|
if stroke_color == 0:
|
|
stroke_width = 0
|
|
elif stroke_width == 0:
|
|
stroke_color = 0
|
|
|
|
try:
|
|
unique_group_commands = process_unique_group_of_lines(
|
|
unique_width_and_color_data,
|
|
translate,
|
|
viewbox_size,
|
|
path_open,
|
|
stroke_width,
|
|
stroke_color,
|
|
fill_color,
|
|
precise,
|
|
raise_error)
|
|
|
|
if unique_group_commands:
|
|
open_paths_commands += unique_group_commands
|
|
except pebble_commands.InvalidPointException:
|
|
error = True
|
|
|
|
return open_paths_commands, error
|
|
|
|
|
|
def get_commands(translate, viewbox_size, frame_data, precise=False, raise_error=False, truncate_color=True):
|
|
commands = []
|
|
errors = []
|
|
|
|
fillGroups_data = frame_data['lineData']
|
|
|
|
# The 'fillGroup' property describes the type of group: A unique letter
|
|
# (e.g. "A", "B", "C" etc.) for a unique fill, and a special identifier
|
|
# for ALL open paths (non-fills)
|
|
only_fills = list([d for d in fillGroups_data if d["fillGroup"] != OPEN_PATH_TAG])
|
|
only_fills = sorted(only_fills, key=lambda f: f["fillGroup"]) # Don't assume data is sorted
|
|
only_open_paths = list([d for d in fillGroups_data if d["fillGroup"] == OPEN_PATH_TAG])
|
|
# Fills must be drawn before open paths, so place them first
|
|
ordered_fill_groups = only_fills + only_open_paths
|
|
|
|
# Process fillGroups
|
|
for path_type, fillGroup in groupby(ordered_fill_groups, lambda b: b['fillGroup']):
|
|
fillGroup_data = list(fillGroup)
|
|
|
|
path_open = path_type == '_'
|
|
if not path_open:
|
|
# Filled fillGroup
|
|
fillGroup_commands, error = process_fill(
|
|
fillGroup_data,
|
|
translate,
|
|
viewbox_size,
|
|
path_open,
|
|
precise,
|
|
raise_error,
|
|
truncate_color)
|
|
else:
|
|
# Open path fillGroup
|
|
fillGroup_commands, error = process_open_paths(
|
|
fillGroup_data,
|
|
translate,
|
|
viewbox_size,
|
|
path_open,
|
|
precise,
|
|
raise_error,
|
|
truncate_color)
|
|
|
|
if error:
|
|
errors += str(path_type)
|
|
elif fillGroup_commands:
|
|
commands += fillGroup_commands
|
|
|
|
if not commands:
|
|
# Insert one 'invisible' command so the frame is valid
|
|
c = pebble_commands.PathCommand([((0.0), (0.0)), ((0.0), (0.0))],
|
|
True,
|
|
translate,
|
|
0,
|
|
0,
|
|
0)
|
|
commands.append(c)
|
|
|
|
return commands, errors
|
|
|
|
|
|
def parse_json_sequence(filename, viewbox_size, precise=False, raise_error=False):
|
|
frames = []
|
|
errors = []
|
|
translate = (0, 0)
|
|
|
|
with open(filename) as json_file:
|
|
try:
|
|
data = json.load(json_file)
|
|
except ValueError:
|
|
print('Invalid JSON format')
|
|
return frames, 0, 0
|
|
|
|
frames_data = data['lineData']
|
|
frame_duration = int(data['compData']['frameDuration'] * 1000)
|
|
for idx, frame_data in enumerate(frames_data):
|
|
cmd_list, frame_errors = get_commands(
|
|
translate,
|
|
viewbox_size,
|
|
frame_data,
|
|
precise,
|
|
raise_error)
|
|
|
|
if frame_errors:
|
|
errors.append((idx, frame_errors))
|
|
elif cmd_list is not None:
|
|
frames.append(cmd_list)
|
|
|
|
return frames, errors, frame_duration
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('path', type=str, help="Path to json file")
|
|
args = parser.parse_args()
|
|
path = os.path.abspath(args.path)
|
|
parse_json_sequence(path)
|